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

/**
 * @fileoverview
 * Construction and parsing of taler:// URIs.
 * Specification: https://lsd.gnunet.org/lsd0006/
 */

/**
 * Imports.
 */
import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { HostPortPath, Paytos } from "./payto.js";
import { Result } from "./result.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { AmountString, EddsaPublicKeyString, HashCodeString } from "./types-taler-common.js";
import { URL, URLSearchParams } from "./url.js";
import {
  opFixedSuccess,
  opKnownFailure,
  opKnownFailureWithBody,
} from "./operation.js";
import { Amounts } from "./amounts.js";
import { assertUnreachable } from "./errors.js";
/**
 * A parsed taler URI.
 */
export type TalerUri =
  | PayUriResult
  | PayTemplateUriResult
  | DevExperimentUri
  | PayPullUriResult
  | PayPushUriResult
  | BackupRestoreUri
  | RefundUriResult
  | WithdrawUriResult
  | WithdrawExchangeUri
  | AddExchangeUri
  | WithdrawalTransferResultUri
  | AddContactUri;

declare const __action_str: unique symbol;
export type TalerUriString = string & { [__action_str]: true };

export function codecForTalerUriString(): Codec<TalerUriString> {
  return {
    decode(x: any, c?: Context): TalerUriString {
      if (typeof x !== "string") {
        throw new DecodingError(
          `expected string at ${renderContext(c)} but got ${typeof x}`,
        );
      }
      if (parseTalerUri(x) === undefined) {
        throw new DecodingError(
          `invalid taler URI at ${renderContext(c)} but got "${x}"`,
        );
      }
      return x as TalerUriString;
    },
  };
}

const TALER_PREFIX = "taler://";
const TALER_HTTP_PREFIX = "taler+http://";

export enum TalerUriParseError {
  /**
   * URI should start with taler:// or taler+http://
   */
  WRONG_PREFIX,
  /**
   * URI should have a / after the target type
   */
  INCOMPLETE,
  /**
   * URI type is not in the list of supported types
   */
  UNSUPPORTED,
  /**
   * The quantity of components is wrong based on the target type
   */
  COMPONENTS_LENGTH,
  /**
   * The validation of one path component failed
   */
  INVALID_TARGET_PATH,
  /**
   * The validation of one parameter component failed
   */
  INVALID_PARAMETER,
}

export namespace TalerUris {
  export type URI = TalerUri;

  const supported_targets: Record<TalerUriAction, true> = {
    "add-contact": true,
    "add-exchange": true,
    "dev-experiment": true,
    pay: true,
    "pay-pull": true,
    "pay-template": true,
    "pay-push": true,
    "withdraw-exchange": true,
    refund: true,
    restore: true,
    withdraw: true,
    "withdrawal-transfer-result": true,
  };

  export function createTalerPay(
    merchantBaseUrl: HostPortPath,
    orderId: string,
    sessionId: string,
    opts: {
      claimToken?: string;
      noncePriv?: string;
    } = {},
  ): PayUriResult {
    return {
      type: TalerUriAction.Pay,
      merchantBaseUrl,
      orderId,
      sessionId,
      ...opts,
    };
  }
  export function createTalerWithdraw(
    bankIntegrationApiBaseUrl: HostPortPath,
    withdrawalOperationId: string,
    opts: {
      externalConfirmation?: boolean;
    } = {},
  ): WithdrawUriResult {
    return {
      type: TalerUriAction.Withdraw,
      bankIntegrationApiBaseUrl,
      withdrawalOperationId,
      ...opts,
    };
  }
  export function createTalerRefund(
    merchantBaseUrl: HostPortPath,
    orderId: string,
  ): RefundUriResult {
    return {
      type: TalerUriAction.Refund,
      merchantBaseUrl,
      orderId,
    };
  }
  export function createTalerPayPull(
    exchangeBaseUrl: HostPortPath,
    contractPriv: string,
  ): PayPullUriResult {
    return {
      type: TalerUriAction.PayPull,
      exchangeBaseUrl,
      contractPriv,
    };
  }
  export function createTalerPayPush(
    exchangeBaseUrl: HostPortPath,
    contractPriv: string,
  ): PayPushUriResult {
    return {
      type: TalerUriAction.PayPush,
      exchangeBaseUrl,
      contractPriv,
    };
  }
  export function createTalerPayTemplate(
    merchantBaseUrl: HostPortPath,
    templateId: string,
  ): PayTemplateUriResult {
    return {
      type: TalerUriAction.PayTemplate,
      merchantBaseUrl,
      templateId,
    };
  }
  export function createTalerRestore(
    walletRootPriv: string,
    providers: HostPortPath[],
  ): BackupRestoreUri {
    return {
      type: TalerUriAction.Restore,
      providers,
      walletRootPriv,
    };
  }
  export function createTalerDevExperiment(
    devExperimentId: string,
    // params: Record<string,string>,
    query: URLSearchParams, // FIXME: Wrong type it should be Record<string,string>
  ): DevExperimentUri {
    return {
      type: TalerUriAction.DevExperiment,
      devExperimentId,
      query,
    };
  }
  export function createTalerWithdrawExchange(
    exchangeBaseUrl: HostPortPath,
    opts: {
      amount?: AmountString;
    } = {},
  ): WithdrawExchangeUri {
    return {
      type: TalerUriAction.WithdrawExchange,
      exchangeBaseUrl,
      ...opts,
    };
  }
  export function createTalerAddExchange(
    exchangeBaseUrl: HostPortPath,
  ): AddExchangeUri {
    return {
      type: TalerUriAction.AddExchange,
      exchangeBaseUrl,
    };
  }
  export function createTalerAddContact(
    alias: string,
    aliasType: string,
    mailboxUri: string,
    mailboxIdentity: string,
    sourceBaseUrl: string,
  ): AddContactUri {
    return {
      type: TalerUriAction.AddContact,
      alias: alias,
      aliasType: aliasType,
      mailboxBaseUri: mailboxUri,
      mailboxIdentity: mailboxIdentity,
      sourceBaseUrl: sourceBaseUrl,
    };
  }
  export function createTalerWithdrawalTransferResult(
    ref: string,
    opts: {
      status?: "success" | "aborted";
    } = {},
  ): WithdrawalTransferResultUri {
    return {
      type: TalerUriAction.WithdrawalTransferResult,
      ref,
      ...opts,
    };
  }
  function asHost(s: HostPortPath): string {
    const b = new URL(s);
    // if (b.port) {
    //   return `${b.host}:${b.port}${b.pathname}`;
    // }
    return `${b.host}${b.pathname}`;
  }

  function getTalerParamList(p: URI): [string, string][] {
    const result: [string, string][] = [];
    switch (p.type) {
      case TalerUriAction.Withdraw: {
        if (p.externalConfirmation) result.push(["external-confirmation", "1"]);
        return result;
      }
      case TalerUriAction.Pay: {
        if (p.claimToken) result.push(["c", p.claimToken]);
        if (p.noncePriv) result.push(["n", p.noncePriv]);
        return result;
      }
      case TalerUriAction.WithdrawExchange: {
        if (p.amount) result.push(["a", p.amount]);
        return result;
      }
      case TalerUriAction.WithdrawalTransferResult: {
        result.push(["ref", p.ref]);
        if (p.status) result.push(["status", p.status]);
        return result;
      }
      case TalerUriAction.AddContact: {
        if (p.sourceBaseUrl) result.push(["sourceBaseUrl", p.sourceBaseUrl]);
        return result;
      }
      case TalerUriAction.Refund:
      case TalerUriAction.PayPush:
      case TalerUriAction.PayPull:
      case TalerUriAction.PayTemplate:
      case TalerUriAction.Restore:
      case TalerUriAction.DevExperiment:
      case TalerUriAction.AddExchange: {
        return result;
      }
      default: {
        assertUnreachable(p);
      }
    }
  }

  function getTalerPrefix(p: URI): string {
    switch (p.type) {
      case TalerUriAction.Withdraw:
        return p.bankIntegrationApiBaseUrl.startsWith("http://")
          ? TALER_HTTP_PREFIX
          : TALER_PREFIX;
      case TalerUriAction.Pay:
      case TalerUriAction.Refund:
      case TalerUriAction.PayTemplate:
        return p.merchantBaseUrl.startsWith("http://")
          ? TALER_HTTP_PREFIX
          : TALER_PREFIX;
      case TalerUriAction.PayPush:
      case TalerUriAction.PayPull:
      case TalerUriAction.AddExchange:
      case TalerUriAction.WithdrawExchange:
        return p.exchangeBaseUrl.startsWith("http://")
          ? TALER_HTTP_PREFIX
          : TALER_PREFIX;
      case TalerUriAction.Restore:
      case TalerUriAction.DevExperiment:
      case TalerUriAction.WithdrawalTransferResult:
      case TalerUriAction.AddContact:
        return TALER_PREFIX;
      default:
        assertUnreachable(p);
    }
  }

  function getTalerPath(p: URI): string {
    /**
     * After the host we should not add a / since the href
     * already adds one
     */
    switch (p.type) {
      case TalerUriAction.Withdraw:
        return `/${asHost(p.bankIntegrationApiBaseUrl)}${
          p.withdrawalOperationId
        }`;
      case TalerUriAction.Pay:
        return `/${asHost(p.merchantBaseUrl)}${p.orderId}/${p.sessionId}`;
      case TalerUriAction.Refund:
        // refund should end with a /
        return `/${asHost(p.merchantBaseUrl)}${p.orderId}/`;
      case TalerUriAction.PayTemplate:
        return `/${asHost(p.merchantBaseUrl)}${p.templateId}`;
      case TalerUriAction.PayPush:
        return `/${asHost(p.exchangeBaseUrl)}${p.contractPriv}`;
      case TalerUriAction.PayPull:
        return `/${asHost(p.exchangeBaseUrl)}${p.contractPriv}`;
      case TalerUriAction.AddExchange:
        return `/${asHost(p.exchangeBaseUrl)}`;
      case TalerUriAction.WithdrawExchange:
        return `/${asHost(p.exchangeBaseUrl)}`;
      case TalerUriAction.Restore:
        return `/${p.walletRootPriv}/${p.providers
          .map((d) => encodeURIComponent(d))
          .join(",")}`;
      case TalerUriAction.DevExperiment:
        return `/${p.devExperimentId}`;
      case TalerUriAction.WithdrawalTransferResult:
        return `/`;
      case TalerUriAction.AddContact:
        return `/${p.aliasType}/${p.alias}/${asHost(p.mailboxBaseUri as HostPortPath)}/${p.mailboxIdentity}`
      default:
        assertUnreachable(p);
    }
  }

  export function toString(p: URI): TalerUriString {
    const prefix = getTalerPrefix(p);
    const path = getTalerPath(p);
    const paramList = getTalerParamList(p);
    const url = new URL(`${prefix}${p.type}${path}`);
    url.search = createSearchParams(paramList);
    return url.href as TalerUriString;
  }

  export function fromString(
    s: string,
    opts: {
      /**
       * do not check path component format
       */
      ignoreComponentError?: boolean;
    } = {},
  ) {
    // check prefix
    let isHttp = false;
    if (
      !s.startsWith(TALER_PREFIX) &&
      !(isHttp = s.startsWith(TALER_HTTP_PREFIX))
    ) {
      return opKnownFailure(TalerUriParseError.WRONG_PREFIX);
    }
    const scheme = isHttp ? ("http" as const) : ("https" as const);

    // get path and search
    const [path, search] = s
      .slice((isHttp ? TALER_HTTP_PREFIX : TALER_PREFIX).length)
      .split("?", 2);

    // check if supported
    const firstSlashPos = path.indexOf("/");
    const uriType = (
      firstSlashPos === -1 ? path : path.slice(0, firstSlashPos)
    ) as TalerUriAction;
    if (!supported_targets[uriType]) {
      const d = opKnownFailureWithBody(TalerUriParseError.UNSUPPORTED, {
        uriType,
      });
      return d;
    }

    const targetPath = path.slice(firstSlashPos + 1);
    if (firstSlashPos === -1 || !targetPath) {
      return opKnownFailureWithBody(TalerUriParseError.INCOMPLETE, {
        uriType,
      });
    }

    // parse params
    const params: { [k: string]: string } = {};
    if (search) {
      const searchParams = new URLSearchParams(search);
      searchParams.forEach((v, k) => {
        // URLSearchParams already decodes uri components
        params[k] = v;
      });
    }

    // get URI components
    const cs = targetPath.split("/");
    switch (uriType) {
      case TalerUriAction.Pay: {
        // check number of segments
        if (cs.length < 3) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        // get merchant host
        const merchant = Paytos.parseHostPortPath2(
          cs[0],
          cs.slice(1, -2).join("/"),
          scheme,
        );
        if (!opts.ignoreComponentError && !merchant) {
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 0 as const,
              uriType,
              error: merchant,
            },
          );
        }

        // get order
        const orderId = cs[cs.length - 2];
        // get session
        const sessionId = cs[cs.length - 1];

        return opFixedSuccess<URI>(
          createTalerPay(
            merchant ?? (cs[0] as HostPortPath),
            orderId,
            sessionId,
            {
              claimToken: params["c"],
              noncePriv: params["n"],
            },
          ),
        );
      }
      case TalerUriAction.Withdraw: {
        // check number of segments
        if (cs.length < 2) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        // get bank host
        const bank = Paytos.parseHostPortPath2(
          cs[0],
          cs.slice(1, -1).join("/"),
          scheme,
        );
        if (!opts.ignoreComponentError && !bank) {
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 0 as const,
              uriType,
              error: bank,
            },
          );
        }

        // get operation id
        const operationId = cs[cs.length - 1];
        // get external confirmation
        const externalConfirmation = !params["external-confirmation"]
          ? undefined
          : params["external-confirmation"] === "1";

        return opFixedSuccess<URI>(
          createTalerWithdraw(bank ?? (cs[0] as HostPortPath), operationId, {
            externalConfirmation,
          }),
        );
      }
      case TalerUriAction.Refund: {
        // check number of segments
        if (cs.length < 3) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }
        if (cs[cs.length - 1]) {
          // last must be empty
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 1 as const,
              uriType,
            },
          );
        }

        // get merchant host
        const merchant = Paytos.parseHostPortPath2(
          cs[0],
          cs.slice(1, -2).join("/"),
          scheme,
        );
        if (!opts.ignoreComponentError && !merchant) {
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 0 as const,
              uriType,
              error: merchant,
            },
          );
        }

        // get order id
        const orderId = cs[cs.length - 2];
        return opFixedSuccess<URI>(
          createTalerRefund(merchant ?? (cs[0] as HostPortPath), orderId),
        );
      }
      case TalerUriAction.PayPull: {
        // check number of segments
        if (cs.length < 2) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        // get exchange host
        const exchange = Paytos.parseHostPortPath2(
          cs[0],
          cs.slice(1, -1).join("/"),
          scheme,
        );
        if (!opts.ignoreComponentError && !exchange) {
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 0 as const,
              uriType,
              error: exchange,
            },
          );
        }
        // get contract priv
        const contractPriv = cs[cs.length - 1]; // FIXME: validate private key

        return opFixedSuccess<URI>(
          createTalerPayPull(exchange ?? (cs[0] as HostPortPath), contractPriv),
        );
      }
      case TalerUriAction.PayPush: {
        // check number of segments
        if (cs.length < 2) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        // get exchange host
        const exchange = Paytos.parseHostPortPath2(
          cs[0],
          cs.slice(1, -1).join("/"),
          scheme,
        );
        if (!opts.ignoreComponentError && !exchange) {
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 0 as const,
              uriType,
              error: exchange,
            },
          );
        }

        // get contract priv
        const contractPriv = cs[cs.length - 1]; // FIXME: validate private key

        return opFixedSuccess<URI>(
          createTalerPayPush(exchange ?? (cs[0] as HostPortPath), contractPriv),
        );
      }
      case TalerUriAction.PayTemplate: {
        // check number of segments
        if (cs.length < 2) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        // get merchant host
        const merchant = Paytos.parseHostPortPath2(
          cs[0],
          cs.slice(1, -1).join("/"),
          scheme,
        );
        if (!opts.ignoreComponentError && !merchant) {
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 0 as const,
              uriType,
              error: merchant,
            },
          );
        }

        // get contract priv
        const contractPriv = cs[cs.length - 1]; // FIXME: validate private key

        return opFixedSuccess<URI>(
          createTalerPayTemplate(
            merchant ?? (cs[0] as HostPortPath),
            contractPriv,
          ),
        );
      }
      case TalerUriAction.Restore: {
        // check number of segments
        if (cs.length !== 2) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        const walletPriv = cs[0]; // FIXME: validate private key
        const providers: Array<HostPortPath> = [];
        // const providers = new Array<HostPortPath>();
        cs[1].split(",").map((name) => {
          const url = decodeURIComponent(name);

          let isHttp = false;
          const withoutScheme = url.startsWith("https://")
            ? url.substring(8)
            : (isHttp = url.startsWith("http://"))
              ? url.substring(7)
              : url;

          // Check resolution of this issue https://bugs.gnunet.org/view.php?id=10466
          const thisScheme =
            url === withoutScheme ? scheme : isHttp ? "http" : "https";

          const [hostname, path] = withoutScheme.split("/", 1);
          const host = Paytos.parseHostPortPath2(
            hostname,
            path,
            thisScheme,
          )!;
          providers.push(host);
        });

        return opFixedSuccess<URI>(
          createTalerRestore(walletPriv ?? (cs[0] as HostPortPath), providers),
        );
      }
      case TalerUriAction.DevExperiment: {
        // check number of segments
        if (cs.length !== 1) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        const experimentId = cs[0];
        const query = new URLSearchParams(search);

        return opFixedSuccess<URI>(
          createTalerDevExperiment(experimentId, query),
        );
      }
      case TalerUriAction.WithdrawExchange: {
        // check number of segments
        if (cs.length < 1) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        // FIXME: https://bugs.gnunet.org/view.php?id=10466
        // if (cs[cs.length-1]) {
        if (cs.length > 1 && cs[cs.length - 1]) {
          // last must be empty
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 1 as const,
              uriType,
            },
          );
        }

        // get exchange host
        const exchange = Paytos.parseHostPortPath2(
          cs[0],
          cs.slice(1, -1).join("/"),
          scheme,
        );
        if (!opts.ignoreComponentError && !exchange) {
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 0 as const,
              uriType,
              error: exchange,
            },
          );
        }

        // get amount param
        const amountRes = !params["a"]
          ? undefined
          : Amounts.parseWithError(params["a"]);
        if (
          !opts.ignoreComponentError &&
          amountRes &&
          amountRes.type === "fail"
        ) {
          return opKnownFailureWithBody(TalerUriParseError.INVALID_PARAMETER, {
            name: "a" as const,
            uriType,
            error: amountRes,
          });
        }
        const amount =
          amountRes && amountRes.type === "ok"
            ? Amounts.stringify(amountRes.body)
            : undefined;

        return opFixedSuccess<URI>(
          createTalerWithdrawExchange(exchange ?? (cs[0] as HostPortPath), {
            amount,
          }),
        );
      }
      case TalerUriAction.AddExchange: {
        // check number of segments
        if (cs.length === 1) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        // get exchange host
        const exchange = Paytos.parseHostPortPath2(
          cs[0],
          cs.slice(1).join("/"),
          scheme,
        );
        if (!opts.ignoreComponentError && !exchange) {
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 0 as const,
              uriType,
              error: exchange,
            },
          );
        }

        return opFixedSuccess<URI>(
          createTalerAddExchange(exchange ?? (cs[0] as HostPortPath)),
        );
      }
      case TalerUriAction.WithdrawalTransferResult: {
        if (cs.length === 0) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }
        const ref = params["ref"];
        const status =
          params["status"] !== "aborted" && params["status"] !== "success"
            ? undefined
            : params["status"];

        return opFixedSuccess<URI>(
          createTalerWithdrawalTransferResult(ref, {
            status,
          }),
        );
      }
      case TalerUriAction.AddContact: {
        // check number of segments
        if (cs.length < 4) {
          return opKnownFailureWithBody(TalerUriParseError.COMPONENTS_LENGTH, {
            uriType,
          });
        }

        const mailboxBaseUri = Paytos.parseHostPortPath2(
          cs[2],
          cs.slice(2, cs.length - 2).join("/"),
          scheme,
        );
        if (!mailboxBaseUri) {
          return opKnownFailureWithBody(
            TalerUriParseError.INVALID_TARGET_PATH,
            {
              pos: 0 as const,
              uriType,
              error: mailboxBaseUri,
            },
          );
        }
        const mailboxIdentity = cs[cs.length - 1];

        return opFixedSuccess<URI>(
          createTalerAddContact(cs[0], cs[1], mailboxBaseUri, mailboxIdentity, params["sourceBaseUrl"]),
        );
      }
      default: {
        assertUnreachable(uriType);
      }
    }
  }
}

/**
 *
 */
export interface PayUriResult {
  type: TalerUriAction.Pay;
  merchantBaseUrl: HostPortPath;
  orderId: string;
  sessionId: string;
  claimToken?: string;
  /**
   * Nonce priv, only present in the
   * "continue on mobile" payment flow.
   */
  noncePriv?: string;
}

export type TemplateParams = {
  amount?: string;
  summary?: string;
};

export interface PayTemplateUriResult {
  type: TalerUriAction.PayTemplate;
  merchantBaseUrl: HostPortPath;
  templateId: string;
}

export interface WithdrawUriResult {
  type: TalerUriAction.Withdraw;
  bankIntegrationApiBaseUrl: HostPortPath;
  withdrawalOperationId: string;
  externalConfirmation?: boolean;
}

export interface RefundUriResult {
  type: TalerUriAction.Refund;
  merchantBaseUrl: HostPortPath;
  orderId: string;
}

export interface PayPushUriResult {
  type: TalerUriAction.PayPush;
  exchangeBaseUrl: HostPortPath;
  contractPriv: string;
}

export interface PayPullUriResult {
  type: TalerUriAction.PayPull;
  exchangeBaseUrl: HostPortPath;
  contractPriv: string;
}

export interface DevExperimentUri {
  type: TalerUriAction.DevExperiment;
  devExperimentId: string;
  query?: URLSearchParams; // FIXME: Wrong type it should be Record<string,string>
}

export interface BackupRestoreUri {
  type: TalerUriAction.Restore;
  walletRootPriv: string;
  providers: Array<HostPortPath>;
}

export interface WithdrawExchangeUri {
  type: TalerUriAction.WithdrawExchange;
  exchangeBaseUrl: HostPortPath;
  amount?: AmountString;
}

export interface AddExchangeUri {
  type: TalerUriAction.AddExchange;
  exchangeBaseUrl: HostPortPath;
}
export interface WithdrawalTransferResultUri {
  type: TalerUriAction.WithdrawalTransferResult;
  ref: string;
  status?: "success" | "aborted";
}

export interface AddContactUri {
  type: TalerUriAction.AddContact;
  alias: string;
  aliasType: string;
  mailboxBaseUri: string;
  mailboxIdentity: HashCodeString;
  sourceBaseUrl: string;
}

/**
 * Parse a taler[+http]://withdraw URI.
 * Return undefined if not passed a valid URI.
 */
export function parseWithdrawUriWithError(s: string) {
  const pi = parseProtoInfoWithError(s, "withdraw");
  if (pi.tag === "error") {
    return pi;
  }

  const c = pi.value.rest.split("?", 2);
  const path = c[0];
  const q = new URLSearchParams(c[1] ?? "");

  const parts = path.split("/");

  if (parts.length < 2) {
    return Result.error(TalerErrorCode.WALLET_TALER_URI_MALFORMED);
  }

  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("/");

  const result: WithdrawUriResult = {
    type: TalerUriAction.Withdraw,
    bankIntegrationApiBaseUrl: Paytos.parseHostPortPath2(
      host,
      pathSegments.join("/"),
      pi.value.innerProto,
    )!,
    withdrawalOperationId: withdrawId,
    externalConfirmation: q.get("external-confirmation") == "1",
  };
  return Result.of(result);
}

/**
 *
 * @deprecated use parseWithdrawUriWithError
 */
export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
  const r = parseWithdrawUriWithError(s);
  if (r.tag === "error") return undefined;
  return r.value;
}

/**
 * Parse a taler[+http]://withdraw URI.
 * Return undefined if not passed a valid URI.
 */
export function parseAddExchangeUriWithError(s: string) {
  const pi = parseProtoInfoWithError(s, "add-exchange");
  if (pi.tag === "error") {
    return pi;
  }
  const parts = pi.value.rest.split("/");

  if (parts.length < 2) {
    return Result.error(TalerErrorCode.WALLET_TALER_URI_MALFORMED);
  }

  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 p = [host, ...pathSegments].join("/");

  const result: AddExchangeUri = {
    type: TalerUriAction.AddExchange,
    exchangeBaseUrl: Paytos.parseHostPortPath2(
      host,
      pathSegments.join("/"),
      pi.value.innerProto,
    )!,
  };
  return Result.of(result);
}

/**
 * Parse a taler[+http]://add-contact URI.
 * Return undefined if not passed a valid URI.
 */
export function parseAddContactUriWithError(s: string) {
  const pi = parseProtoInfoWithError(s, "add-contact");
  if (pi.tag === "error") {
    return pi;
  }
  const parts = pi.value.rest.split("/");

  if (parts.length < 4) {
    return Result.error(TalerErrorCode.WALLET_TALER_URI_MALFORMED);
  }
  const mailboxBaseUri = parts[2];
  const pathSegments = parts.slice(3, parts.length - 2);
  const lastPart = parts[parts.length-1];
  const q = new URLSearchParams(lastPart ?? "");
  const mailboxIdentity = lastPart.split("?")[0];
  const sourceBaseUrl = q.get("sourceBaseUrl") ?? "";
  const mailboxHostPort = Paytos.parseHostPortPath2(
    mailboxBaseUri,
    pathSegments.join("/"),
    pi.value.innerProto,
  );
  const result: AddContactUri = {
    type: TalerUriAction.AddContact,
    aliasType: parts[0],
    alias: parts[1],
    mailboxBaseUri: mailboxHostPort!,
    mailboxIdentity: mailboxIdentity,
    sourceBaseUrl: sourceBaseUrl,
  };
  return Result.of(result);
}

/**
 *
 * @deprecated use parseWithdrawUriWithError
 */
export function parseAddExchangeUri(s: string): AddExchangeUri | undefined {
  const r = parseAddExchangeUriWithError(s);
  if (r.tag === "error") return undefined;
  return r.value;
}

/**
 *
 * @deprecated use parseWithdrawUriWithError
 */
export function parseAddContactUri(s: string): AddContactUri | undefined {
  const r = parseAddContactUriWithError(s);
  if (r.tag === "error") return undefined;
  return r.value;
}

export enum TalerUriAction {
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.1
   */
  Withdraw = "withdraw",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.2
   */
  Pay = "pay",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.3
   */
  Refund = "refund",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.4
   */
  PayPush = "pay-push",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.5
   */
  PayPull = "pay-pull",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.6
   */
  PayTemplate = "pay-template",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.7
   */
  Restore = "restore",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.8
   */
  DevExperiment = "dev-experiment",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.9
   */
  AddExchange = "add-exchange",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.10
   */
  WithdrawExchange = "withdraw-exchange",
  /**
   * https://lsd.gnunet.org/lsd0006/#section-5.11
   */
  WithdrawalTransferResult = "withdrawal-transfer-result",
  /**
   * FIXME: LSD
   * Add a contact to the wallet
   */
  AddContact = "add-contact"
}

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

interface ProtoInfo {
  innerProto: "http" | "https";
  rest: string;
}

/**
 * @deprecated
 *
 * @param s
 * @param action
 * @returns
 */
function parseProtoInfoWithError(
  s: string,
  action: string,
): Result<ProtoInfo, TalerErrorCode.WALLET_TALER_URI_MALFORMED> {
  if (
    !s.toLowerCase().startsWith("taler://") &&
    !s.toLowerCase().startsWith("taler+http://")
  ) {
    return Result.error(TalerErrorCode.WALLET_TALER_URI_MALFORMED);
  }
  const pfxPlain = `taler://${action}/`;
  const pfxHttp = `taler+http://${action}/`;
  if (s.toLowerCase().startsWith(pfxPlain)) {
    return Result.of({
      innerProto: "https",
      rest: s.substring(pfxPlain.length),
    });
  } else if (s.toLowerCase().startsWith(pfxHttp)) {
    return Result.of({
      innerProto: "http",
      rest: s.substring(pfxHttp.length),
    });
  } else {
    return Result.error(TalerErrorCode.WALLET_TALER_URI_MALFORMED);
  }
}

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.Withdraw]: parseWithdrawUri,
  [TalerUriAction.DevExperiment]: parseDevExperimentUri,
  [TalerUriAction.WithdrawExchange]: parseWithdrawExchangeUri,
  [TalerUriAction.AddExchange]: parseAddExchangeUri,
  [TalerUriAction.AddContact]: parseAddContactUri,
  [TalerUriAction.WithdrawalTransferResult]: () => {
    throw new Error("not supported");
  },
};

/**
 * @deprecated
 *
 * @param string
 * @returns
 */
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);
}

/**
 * @deprecated
 *
 * @param uri
 * @returns
 */
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.Withdraw: {
      return stringifyWithdrawUri(uri);
    }
    case TalerUriAction.WithdrawExchange: {
      return stringifyWithdrawExchange(uri);
    }
    case TalerUriAction.AddExchange: {
      return stringifyAddExchange(uri);
    }
    case TalerUriAction.AddContact: {
      return stringifyAddContact(uri);
    }
    case TalerUriAction.WithdrawalTransferResult: {
      throw Error("not supported");
    }
  }
}

/**
 * @deprecated
 *
 * 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 = Paytos.parseHostPortPath2(
    host,
    pathSegments.join("/"),
    pi.innerProto,
  )!;

  return {
    type: TalerUriAction.Pay,
    merchantBaseUrl,
    orderId,
    sessionId,
    claimToken,
    noncePriv,
  };
}

/**
 * @deprecated
 *
 * @param s
 * @returns
 */
export function parsePayTemplateUri(
  uriString: string,
): PayTemplateUriResult | undefined {
  const pi = parseProtoInfo(uriString, TalerUriAction.PayTemplate);
  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<string, string> = {};
  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}/`,
  // );

  const merchantBaseUrl = Paytos.parseHostPortPath2(
    host,
    pathSegments.join("/"),
    pi.innerProto,
  )!;
  return {
    type: TalerUriAction.PayTemplate,
    merchantBaseUrl,
    templateId,
  };
}

/**
 * @deprecated
 *
 * @param s
 * @returns
 */
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
  const pi = parseProtoInfo(s, TalerUriAction.PayPush);
  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}/`,
  // );
  const exchangeBaseUrl = Paytos.parseHostPortPath2(
    host,
    pathSegments.join("/"),
    pi.innerProto,
  )!;

  return {
    type: TalerUriAction.PayPush,
    exchangeBaseUrl,
    contractPriv,
  };
}

/**
 * @deprecated
 *
 * @param s
 * @returns
 */
export function parsePayPullUri(s: string): PayPullUriResult | undefined {
  const pi = parseProtoInfo(s, TalerUriAction.PayPull);
  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}/`,
  // );
  const exchangeBaseUrl = Paytos.parseHostPortPath2(
    host,
    pathSegments.join("/"),
    pi.innerProto,
  )!;

  return {
    type: TalerUriAction.PayPull,
    exchangeBaseUrl,
    contractPriv,
  };
}

/**
 * @deprecaed
 *
 * @param s
 * @returns
 */
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 < 1) {
    return undefined;
  }
  const host = parts[0].toLowerCase();
  // Used to be the reserve public key, now it's empty!
  const lastPathComponent =
    parts.length > 1 ? parts[parts.length - 1] : undefined;

  if (lastPathComponent) {
    // invalid taler://withdraw-exchange URI, must end with a slash
    return undefined;
  }
  const pathSegments = parts.slice(1, parts.length - 1);
  const hostAndSegments = [host, ...pathSegments].join("/");
  // const exchangeBaseUrl = canonicalizeBaseUrl(
  //   `${pi.innerProto}://${hostAndSegments}/`,
  // );
  const exchangeBaseUrl = Paytos.parseHostPortPath2(
    host,
    pathSegments.join("/"),
    pi.innerProto,
  )!;

  const q = new URLSearchParams(c[1] ?? "");
  const amount = (q.get("a") ?? undefined) as AmountString | undefined;

  return {
    type: TalerUriAction.WithdrawExchange,
    exchangeBaseUrl,
    amount,
  };
}

/**
 * @deprecated
 * 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}/`,
  // );
  const merchantBaseUrl = Paytos.parseHostPortPath2(
    host,
    pathSegments.join("/"),
    pi.innerProto,
  )!;

  return {
    type: TalerUriAction.Refund,
    merchantBaseUrl,
    orderId,
  };
}

/**
 * @deprecated
 *
 * @param s
 * @returns
 */
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],
    query: new URLSearchParams(c[1] ?? ""),
  };
}

/**
 * @deprecated
 *
 * @param s
 * @returns
 */
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<HostPortPath>();
  parts[1].split(",").map((name) => {
    const url = decodeURIComponent(name);
    let isHttp = false;
    const withoutScheme = url.startsWith("https://")
      ? url.substring(8)
      : (isHttp = url.startsWith("http://"))
        ? url.substring(7)
        : url;
    const scheme =
      url === withoutScheme ? pi.innerProto : isHttp ? "http" : "https";
    const [hostname, path] = withoutScheme.split("/", 1);
    const host = Paytos.parseHostPortPath2(hostname, path ?? "/", scheme)!;
    providers.push(host);
  });
  return {
    type: TalerUriAction.Restore,
    walletRootPriv,
    providers,
  };
}

// ================================================
//  To string functions
// ================================================

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyPayUri({
  merchantBaseUrl,
  orderId,
  sessionId,
  claimToken,
  noncePriv,
}: Omit<PayUriResult, "type">): string {
  const { proto, path, query } = getUrlInfo(merchantBaseUrl, {
    c: claimToken,
    n: noncePriv,
  });
  return `${proto}://pay/${path}${orderId}/${sessionId}${query}`;
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyPayPullUri({
  contractPriv,
  exchangeBaseUrl,
}: Omit<PayPullUriResult, "type">): string {
  const { proto, path } = getUrlInfo(exchangeBaseUrl);
  return `${proto}://pay-pull/${path}${contractPriv}`;
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyPayPushUri({
  contractPriv,
  exchangeBaseUrl,
}: Omit<PayPushUriResult, "type">): string {
  const { proto, path } = getUrlInfo(exchangeBaseUrl);

  return `${proto}://pay-push/${path}${contractPriv}`;
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyRestoreUri({
  providers,
  walletRootPriv,
}: Omit<BackupRestoreUri, "type">): string {
  const list = providers
    .map((url) => `${encodeURIComponent(new URL(url).href)}`)
    .join(",");
  return `taler://restore/${walletRootPriv}/${list}`;
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyWithdrawExchange({
  exchangeBaseUrl,
  amount,
}: Omit<WithdrawExchangeUri, "type">): string {
  const { proto, path, query } = getUrlInfo(exchangeBaseUrl, {
    a: amount,
  });
  return `${proto}://withdraw-exchange/${path}${query}`;
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyAddExchange({
  exchangeBaseUrl,
}: Omit<AddExchangeUri, "type">): string {
  const { proto, path } = getUrlInfo(exchangeBaseUrl);
  return `${proto}://add-exchange/${path}`;
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyAddContact({
  alias,
  aliasType,
  mailboxBaseUri: mailboxBaseUri,
  mailboxIdentity: mailboxIdentity,
  sourceBaseUrl,
}: Omit<AddContactUri, "type">): string {
  const { proto, path } = getUrlInfo(mailboxBaseUri);
  const baseUri = `${proto}://add-contact/${aliasType}/${alias}/${path}${mailboxIdentity}`;
  if (sourceBaseUrl) {
    return baseUri + `?sourceBaseUrl=${encodeURIComponent(sourceBaseUrl)}`;
  } else {
    return baseUri;
  }
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyDevExperimentUri({
  devExperimentId,
}: Omit<DevExperimentUri, "type">): string {
  return `taler://dev-experiment/${devExperimentId}`;
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyPayTemplateUri({
  merchantBaseUrl,
  templateId,
}: Omit<PayTemplateUriResult, "type">): string {
  const { proto, path, query } = getUrlInfo(merchantBaseUrl);
  return `${proto}://pay-template/${path}${templateId}${query}`;
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyRefundUri({
  merchantBaseUrl,
  orderId,
}: Omit<RefundUriResult, "type">): string {
  const { proto, path } = getUrlInfo(merchantBaseUrl);
  return `${proto}://refund/${path}${orderId}/`;
}

/**
 * @deprecated
 * @param param0
 * @returns
 */
export function stringifyWithdrawUri({
  bankIntegrationApiBaseUrl,
  withdrawalOperationId,
}: Omit<WithdrawUriResult, "type">): string {
  const { proto, path } = getUrlInfo(bankIntegrationApiBaseUrl);
  return `${proto}://withdraw/${path}${withdrawalOperationId}`;
}

export function getURLHostnamePortPath(baseUrl: string) {
  const path = getUrlInfo(baseUrl).path;
  if (path.endsWith("/")) {
    return path.substring(0, path.length - 1);
  }
  return path;
}

/**
 * Use baseUrl to defined http or https
 * create path using host+port+pathname
 * use params to create a query parameter string or empty
 */
function getUrlInfo(
  baseUrl: string,
  params: Record<string, string | undefined> = {},
): { 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 };
}

/**
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
 */
function encodeRFC3986URIComponent(str: string): string {
  return encodeURIComponent(str).replace(
    /[!'()*]/g,
    (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
  );
}
const rfc3986 = encodeRFC3986URIComponent;

/**
 *
 * https://www.rfc-editor.org/rfc/rfc3986
 */
function createSearchParams(paramList: [string, string][]): string {
  return paramList
    .map(([key, value]) => `${rfc3986(key)}=${rfc3986(value)}`)
    .join("&");
}
