/*
 This file is part of GNU Taler
 (C) 2019 GNUnet e.V.

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

import { BitcoinBech32 } from "./bech32.js";
import { generateFakeSegwitAddress } from "./bitcoin.js";
import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { assertUnreachable } from "./errors.js";
import { IbanString, parseIban } from "./iban.js";
import {
  opFixedSuccess,
  opKnownFailure,
  opKnownFailureWithBody,
  succeedOrThrow,
} from "./operation.js";
import {
  decodeCrock,
  encodeCrock,
  hashTruncate32,
  stringToBytes,
} from "./taler-crypto.js";
import { URLSearchParams } from "./url.js";

/**
 * @deprecated use NormalizedPayto or FullPayto
 */
export type PaytoString = string;

const PAYTO_PREFIX = "payto://";

export enum PaytoType {
  IBAN = "iban",
  Bitcoin = "bitcoin",
  TalerBank = "x-taler-bank",
  TalerReserve = "taler-reserve",
  TalerReserveHttp = "taler-reserve-http",
  Ethereum = "ethereum",
}

export enum ReservePubParseError {
  /**
   * It should be 52 characters
   */
  WRONG_LENGTH,
  DECODE_ERROR,
}
declare const __hostport_str: unique symbol;
export type HostPortPath = string & { [__hostport_str]: true };

declare const __btaddr_str: unique symbol;
export type BtAddrString = string & { [__btaddr_str]: true };
declare const __ethaddr_str: unique symbol;
export type EthAddrString = string & { [__ethaddr_str]: true };

export enum PaytoParseError {
  /**
   * Payto should start with payto://
   */
  WRONG_PREFIX,
  /**
   * Payto should have a / after the target type
   */
  INCOMPLETE,
  /**
   * Target 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 or more path components failed
   */
  INVALID_TARGET_PATH,
}

// TODO: use unique symbol to remove compat with string
// TODO: verify in account aml types if we can merge it since this is also an aml account
export type PaytoHash = string;

export namespace Paytos {
  export type URI =
    | PaytoUnsupported
    | PaytoIBAN
    | PaytoTalerReserve
    | PaytoTalerReserveHttp
    | PaytoTalerBank
    | PaytoEthereum
    | PaytoBitcoin;

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

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

  interface PaytoGeneric {
    /**
     * String after the prefix and before the first /
     */
    targetType: PaytoType | undefined;
    /**
     * String after the first /
     */
    normalizedPath: string;
    /**
     * String after the first /
     */
    fullPath: string;
    /**
     * Return the account identification when the target type is already known. Useful to show in the UI
     */
    displayName: string;
    /**
     * All the URL params after the first ?
     */
    params: { [name: string]: string };
  }

  export interface PaytoUnsupported extends PaytoGeneric {
    targetType: undefined;
    target: string;
  }

  export interface PaytoIBAN extends PaytoGeneric {
    targetType: PaytoType.IBAN;
    iban: IbanString;
    bic?: string;
  }

  export interface PaytoTalerReserve extends PaytoGeneric {
    targetType: PaytoType.TalerReserve;
    exchange: HostPortPath;
    reservePub: Uint8Array;
  }

  export interface PaytoTalerReserveHttp extends PaytoGeneric {
    targetType: PaytoType.TalerReserveHttp;
    exchange: HostPortPath;
    reservePub: Uint8Array;
  }

  export interface PaytoTalerBank extends PaytoGeneric {
    targetType: PaytoType.TalerBank;
    /**
     * this is kept to keep compatiblity with old parser
     * @deprecated use URL
     */
    host: string;
    url: HostPortPath;
    account: string;
  }

  export interface PaytoBitcoin extends PaytoGeneric {
    targetType: PaytoType.Bitcoin;
    address: BtAddrString;
    reservePub: Uint8Array | undefined;
    segwitAddrs: Array<BtAddrString>;
  }

  export interface PaytoEthereum extends PaytoGeneric {
    targetType: PaytoType.Ethereum;
    address: EthAddrString;
  }

  const supported_targets: Record<PaytoType, true> = {
    iban: true,
    bitcoin: true,
    "x-taler-bank": true,
    "taler-reserve": true,
    "taler-reserve-http": true,
    ethereum: true,
  };

  export function hash(p: NormalizedPaytoString | FullPaytoString): Uint8Array {
    return hashTruncate32(stringToBytes(p + "\0"));
  }

  /**
   * A **normalized** payto-URI uniquely identifies a bank account (or
   * wallet) and must be able to serve as a canonical representation of such a
   * bank account.  Thus, optional arguments such as the *receiver-name* or
   * optional path components such as the BIC must be removed and the account
   * must be given in a canonical form for the wire method (for example,
   * everything in lower-case)
   *
   * @param p
   * @returns
   */
  export function toNormalizedString(p: URI): NormalizedPaytoString {
    const url = new URL(`${PAYTO_PREFIX}${p.targetType}/${p.normalizedPath}`);
    return url.href as NormalizedPaytoString;
  }
  /**
   * A **full** payto-URI is not expected to have a canonical form for
   * a bank account (there can be many full payto-URIs for the same bank
   * account) and must include at least the *receiver-name* but possibly also
   * other (in RFC 8905 optional) arguments to identify the recipient, as
   * those may be needed to do a wire transfer.
   *
   * @param p
   * @returns
   */
  export function toFullString(p: URI): FullPaytoString {
    const url = new URL(`${PAYTO_PREFIX}${p.targetType}/${p.fullPath}`);
    const paramList = !p.params ? [] : Object.entries(p.params);
    url.search = createSearchParams(paramList);
    return url.href as FullPaytoString;
  }

  export function parseReservePub(reserve: string | undefined) {
    if (!reserve) return opKnownFailure(ReservePubParseError.WRONG_LENGTH);
    try {
      const pub = decodeCrock(reserve);
      if (!pub || pub.length !== 32) {
        return opKnownFailure(ReservePubParseError.WRONG_LENGTH);
      }
      return opFixedSuccess(pub);
    } catch (e) {
      return opKnownFailureWithBody(ReservePubParseError.DECODE_ERROR, {
        message: String(e),
      });
    }
  }
  /**
   * Check hostname is a valid string with form $host:$port/$path like
   * domain.com:22/some/path
   *
   * Return the canonical form.
   *
   * FIXME: new need a function that only takes una string: host+port+path and
   * parse it without using URL to prevent parsing unnecesary components and
   * better error reporting
   * https://bugs.gnunet.org/view.php?id=10467
   *
   * @param hostname
   * @param path
   * @param scheme
   * @returns
   */
  export function parseHostPortPath2(
    hostname: string,
    path: string | undefined,
    scheme: "http" | "https" = "https",
  ): HostPortPath | undefined {
    // maybe it should check that it doesn't contain search or hash?
    try {
      // https://url.spec.whatwg.org/#concept-basic-url-parser
      if (path === undefined) {
        path = "";
      }
      if (!path.endsWith("/")) {
        path = path + "/";
      }
      const url = new URL(path, `${scheme}://${hostname.toLowerCase()}`);
      url.search = "";
      url.password = "";
      url.username = "";
      url.hash = "";
      return url.href as HostPortPath;
    } catch (e) {
      console.log(e);
      return undefined;
    }
  }
  function withoutScheme(h: HostPortPath): HostPortPath {
    return (
      h.startsWith("http://")
        ? h.substring(7)
        : h.startsWith("https://")
          ? h.substring(8)
          : h
    ) as HostPortPath;
  }
  /**
   * Same as `parseHostPortPath2` but only takes one string.
   * This should be the definitive signature.
   * https://bugs.gnunet.org/view.php?id=10467
   *
   * @param hostnameAndPath
   * @returns
   */
  export function parseHostPortPath(
    hostnameAndPath: string,
  ): HostPortPath | undefined {
    const [host, path] = hostnameAndPath.split("/", 1);
    return parseHostPortPath2(host, path ?? "");
  }
  /**
   * FIXME: add ethereum address validator
   * @param str
   */
  export function parseEthereumAddress(str: String): EthAddrString | undefined {
    if (!str) {
      return undefined;
    }
    return str as EthAddrString;
  }

  /**
   * FIXME: add bank account name validation
   *
   * @param account
   * @returns
   */
  export function parseTalerBankAccount(account: string): string | undefined {
    if (!account) {
      return undefined;
    }
    return account;
  }
  //////////////////
  // function to create objs
  //////////////////
  export function createUnsupported(
    targetType: string,
    path: string,
    params: Record<string, string> = {},
  ): PaytoUnsupported {
    return {
      targetType: undefined,
      target: targetType,
      params,
      normalizedPath: path.toLocaleLowerCase(),
      fullPath: path,
      displayName: path,
    };
  }
  export function createIban(
    iban: IbanString,
    bic: string | undefined,
    params: Record<string, string> = {},
  ): PaytoIBAN {
    iban = iban.toUpperCase() as IbanString;
    return {
      targetType: PaytoType.IBAN,
      iban,
      bic,
      params,
      normalizedPath: iban,
      fullPath: !bic ? iban : `${bic}/${iban}`,
      displayName: iban,
    };
  }
  export function createBitcoin(
    address: BtAddrString,
    reservePub: Uint8Array | undefined,
    params: Record<string, string> = {},
  ): PaytoBitcoin {
    const sgRes = !reservePub
      ? undefined
      : generateFakeSegwitAddress(reservePub, address);

    const segwitAddrs = !sgRes || sgRes.type === "fail" ? [] : sgRes.body;
    return {
      targetType: PaytoType.Bitcoin,
      address,
      reservePub,
      segwitAddrs,
      params,
      normalizedPath: address.toLocaleLowerCase(),
      fullPath: !reservePub ? address : `${address}/${encodeCrock(reservePub)}`,
      displayName: address,
    };
  }
  export function createEthereum(
    address: EthAddrString,
    params: Record<string, string> = {},
  ): PaytoEthereum {
    return {
      targetType: PaytoType.Ethereum,
      address,
      params,
      normalizedPath: address,
      fullPath: address,
      displayName: address,
    };
  }
  export function createTalerReserve(
    exchange: HostPortPath,
    reservePub: Uint8Array,
    params: Record<string, string> = {},
  ): PaytoTalerReserve {
    const path = withoutScheme(exchange);
    const pub = encodeCrock(reservePub);
    return {
      targetType: PaytoType.TalerReserve,
      exchange,
      reservePub,
      params,
      normalizedPath: `${path.toLocaleLowerCase()}${pub}`,
      fullPath: `${path}${pub}`,
      displayName: `${path}@${pub}`,
    };
  }
  export function createTalerReserveHttp(
    exchange: HostPortPath,
    reservePub: Uint8Array,
    params: Record<string, string> = {},
  ): PaytoTalerReserveHttp {
    const path = withoutScheme(exchange);
    const pub = encodeCrock(reservePub);
    return {
      targetType: PaytoType.TalerReserveHttp,
      exchange,
      reservePub,
      params,
      normalizedPath: `${path.toLocaleLowerCase()}${pub}`,
      fullPath: `${path}${pub}`,
      displayName: `${path}@${pub}`,
    };
  }
  export function createTalerBank(
    url: HostPortPath,
    account: string,
    params: Record<string, string> = {},
  ): PaytoTalerBank {
    const path = withoutScheme(url);
    const host = path.endsWith("/") ? path.substring(0, path.length - 1) : path;
    return {
      targetType: PaytoType.TalerBank,
      host,
      url,
      account,
      params,
      normalizedPath: `${path.toLocaleLowerCase()}${account}`,
      fullPath: `${path}${account}`,
      displayName: `${account}@${url}`,
    };
  }

  //////////////////////
  // parsing function
  ///////////////////////
  export function asString(p: FullPaytoString): Paytos.URI {
    return succeedOrThrow(fromString(p))
  }

  export function fromString(
    s: string,
    opts: {
      /**
       * do not check path component format
       */
      ignoreComponentError?: boolean;
      /**
       * take unknown target types as valid
       */
      allowUnsupported?: boolean;
    } = {},
  ) {
    if (!s.startsWith(PAYTO_PREFIX)) {
      return opKnownFailure(PaytoParseError.WRONG_PREFIX);
    }

    const [acct, search] = s.slice(PAYTO_PREFIX.length).split("?", 2);

    const firstSlashPos = acct.indexOf("/");

    const targetType = (
      firstSlashPos === -1 ? acct : acct.slice(0, firstSlashPos)
    ) as PaytoType;
    if (!opts.allowUnsupported && !supported_targets[targetType]) {
      const d = opKnownFailureWithBody(PaytoParseError.UNSUPPORTED, {
        targetType,
      });
      return d;
    }
    const targetPath = acct.slice(firstSlashPos + 1);
    if (firstSlashPos === -1 || !targetPath) {
      return opKnownFailureWithBody(PaytoParseError.INCOMPLETE, { targetType });
    }

    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 (targetType) {
      case PaytoType.IBAN: {
        if (cs.length !== 1 && cs.length !== 2) {
          return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, {
            targetType,
          });
        }
        const bic = cs.length === 2 ? cs[0] : undefined;
        const iban = cs.length === 1 ? cs[0] : cs[1];

        const ibaRes = parseIban(iban);

        if (!opts.ignoreComponentError && ibaRes.type === "fail") {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 0,
            targetType,
            error: ibaRes,
          });
        }

        return opFixedSuccess<URI>(createIban(iban as IbanString, bic, params));
      }
      case PaytoType.Bitcoin: {
        if (cs.length !== 1 && cs.length !== 2) {
          return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, {
            targetType,
          });
        }

        const address = cs[0].toLocaleLowerCase();
        const btRes = BitcoinBech32.decode(
          address,
          BitcoinBech32.Encodings.BECH32,
        );
        if (!opts.ignoreComponentError && btRes.type === "fail") {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 0 as const,
            targetType,
            error: btRes,
          });
        }

        const pubRes = cs.length === 1 ? undefined : parseReservePub(cs[1]);
        if (!opts.ignoreComponentError && pubRes && pubRes.type === "fail") {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 1 as const,
            targetType,
            error: pubRes,
          });
        }

        return opFixedSuccess<URI>(
          createBitcoin(
            address as BtAddrString,
            pubRes && pubRes.type === "ok" ? pubRes.body : undefined,
            params,
          ),
        );
      }

      case PaytoType.TalerBank: {
        if (cs.length < 2) {
          return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, {
            targetType,
          });
        }

        const host = parseHostPortPath2(cs[0], cs.slice(1, -1).join("/"));
        if (!opts.ignoreComponentError && !host) {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 0 as const,
            targetType,
            error: host,
          });
        }
        const account = parseTalerBankAccount(cs[cs.length - 1]);
        if (!opts.ignoreComponentError && !account) {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 1 as const,
            targetType,
            error: account,
          });
        }

        return opFixedSuccess<URI>(
          createTalerBank(
            host ?? (cs[0] as HostPortPath),
            account ?? cs[1],
            params,
          ),
        );
      }
      case PaytoType.TalerReserve: {
        if (cs.length < 2) {
          return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, {
            targetType,
          });
        }
        const exchange = parseHostPortPath2(cs[0], cs.slice(1, -1).join("/"));
        if (!opts.ignoreComponentError && !exchange) {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 0 as const,
            targetType,
            error: exchange,
          });
        }

        const reservePub = cs[cs.length - 1];
        const pubRes = parseReservePub(reservePub);
        if (!opts.ignoreComponentError && pubRes.type === "fail") {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 1 as const,
            targetType,
            error: pubRes,
          });
        }

        return opFixedSuccess<URI>(
          createTalerReserve(
            exchange ?? (cs[0] as HostPortPath),
            pubRes.type === "ok" ? pubRes.body : decodeCrock(reservePub),
            params,
          ),
        );
      }
      case PaytoType.TalerReserveHttp: {
        if (cs.length < 2) {
          return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, {
            targetType,
          });
        }
        const exchange = parseHostPortPath2(
          cs[0],
          cs.slice(1, -1).join("/"),
          "http",
        );
        if (!opts.ignoreComponentError && !exchange) {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 0,
            targetType,
            error: exchange,
          });
        }

        const reservePub = cs[cs.length - 1];
        const pubRes = parseReservePub(reservePub);
        if (!opts.ignoreComponentError && pubRes.type === "fail") {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 1,
            targetType,
            error: pubRes,
          });
        }
        return opFixedSuccess<URI>(
          createTalerReserveHttp(
            exchange ?? (cs[0] as HostPortPath),
            pubRes.type === "ok" ? pubRes.body : decodeCrock(reservePub),
            params,
          ),
        );
      }
      case PaytoType.Ethereum: {
        if (cs.length !== 1) {
          return opKnownFailureWithBody(PaytoParseError.COMPONENTS_LENGTH, {
            targetType,
          });
        }
        const address = parseEthereumAddress(cs[0]);
        if (!opts.ignoreComponentError && !address) {
          return opKnownFailureWithBody(PaytoParseError.INVALID_TARGET_PATH, {
            pos: 0,
            targetType,
            error: address,
          });
        }
        return opFixedSuccess<URI>(
          createEthereum(address ?? (cs[0] as EthAddrString), params),
        );
      }
      default: {
        if (opts.allowUnsupported) {
          return opFixedSuccess<URI>(
            createUnsupported(targetType, targetPath, params),
          );
        }
        assertUnreachable(targetType);
      }
    }
  }
}

export function codecForPaytoHash(): Codec<PaytoHash> {
  return {
    decode(x: any, c?: Context): PaytoHash {
      // TODO: implement a stronger validation and reject invalid hash
      // maybe check charset and also length
      if (typeof x !== "string") {
        throw new DecodingError(
          `expected string at ${renderContext(c)} but got ${typeof x}`,
        );
      }
      return x as PaytoHash;
    },
  };
}
export function codecFullForPaytoString(): Codec<Paytos.FullPaytoString> {
  return {
    decode(x: any, c?: Context): Paytos.FullPaytoString {
      // TODO: implement a stronger validation and reject invalid paytos
      if (typeof x !== "string") {
        throw new DecodingError(
          `expected string at ${renderContext(c)} but got ${typeof x}`,
        );
      }
      if (!x.startsWith(PAYTO_PREFIX)) {
        throw new DecodingError(
          `expected start with payto at ${renderContext(c)} but got "${x}"`,
        );
      }
      return x as Paytos.FullPaytoString;
    },
  };
}

export function codecNormalizedForPaytoString(): Codec<Paytos.NormalizedPaytoString> {
  return {
    decode(x: any, c?: Context): Paytos.NormalizedPaytoString {
      // TODO: implement a stronger validation and reject invalid paytos
      if (typeof x !== "string") {
        throw new DecodingError(
          `expected string at ${renderContext(c)} but got ${typeof x}`,
        );
      }
      if (!x.startsWith(PAYTO_PREFIX)) {
        throw new DecodingError(
          `expected start with payto at ${renderContext(c)} but got "${x}"`,
        );
      }
      return x as Paytos.NormalizedPaytoString;
    },
  };
}

/**
 * @deprecated use Paytos namespace
 */
export type PaytoUri =
  | PaytoUriUnknown
  | PaytoUriIBAN
  | PaytoUriTaler
  | PaytoUriTalerHttp
  | PaytoUriTalerBank
  | PaytoUriEthereum
  | PaytoUriBitcoin;

/**
 * @deprecated use codecForNormalizedPAyto or codecForFullPayto
 * @returns
 */
export function codecForPaytoString(): Codec<PaytoString> {
  return {
    decode(x: any, c?: Context): PaytoString {
      if (typeof x !== "string") {
        throw new DecodingError(
          `expected string at ${renderContext(c)} but got ${typeof x}`,
        );
      }
      if (!x.startsWith(PAYTO_PREFIX)) {
        throw new DecodingError(
          `expected start with payto at ${renderContext(c)} but got "${x}"`,
        );
      }
      return x as PaytoString;
    },
  };
}
/**
 * @deprecated use Paytos namespace
 */
export interface PaytoUriGeneric {
  targetType: PaytoType | string;
  targetPath: string;
  params: { [name: string]: string };
}

/**
 * @deprecated use Paytos namespace
 */
export interface PaytoUriUnknown extends PaytoUriGeneric {
  isKnown: false;
}

/**
 * @deprecated use Paytos namespace
 */
export interface PaytoUriIBAN extends PaytoUriGeneric {
  isKnown: true;
  targetType: "iban";
  iban: string;
  bic?: string;
}

/**
 * @deprecated use Paytos namespace
 */
export interface PaytoUriTaler extends PaytoUriGeneric {
  isKnown: true;
  targetType: "taler-reserve";
  exchange: string;
  reservePub: string;
}

/**
 * @deprecated use Paytos namespace
 */
export interface PaytoUriTalerHttp extends PaytoUriGeneric {
  isKnown: true;
  targetType: "taler-reserve-http";
  exchange: string;
  reservePub: string;
}

/**
 * @deprecated use Paytos namespace
 */
export interface PaytoUriTalerBank extends PaytoUriGeneric {
  isKnown: true;
  targetType: "x-taler-bank";
  host: string;
  account: string;
}

/**
 * @deprecated use Paytos namespace
 */
export interface PaytoUriBitcoin extends PaytoUriGeneric {
  isKnown: true;
  targetType: "bitcoin";
  address: string;
  segwitAddrs: Array<string>;
}

/**
 * @deprecated use Paytos namespace
 */
export interface PaytoUriEthereum extends PaytoUriGeneric {
  isKnown: true;
  targetType: "ethereum";
  address: string;
}

/**
 * Add query parameters to a payto URI.
 *
 * Existing parameters are preserved.
 */
export function addPaytoQueryParams(
  s: string,
  params: { [name: string]: string },
): string {
  const [acct, search] = s.slice(PAYTO_PREFIX.length).split("?");
  const searchParams = new URLSearchParams(search || "");
  for (const [paramKey, paramValue] of Object.entries(params)) {
    searchParams.set(paramKey, paramValue);
  }
  const paramList = [...searchParams.entries()];
  if (paramList.length === 0) {
    return PAYTO_PREFIX + acct;
  }
  return PAYTO_PREFIX + acct + "?" + createSearchParams(paramList);
}

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

/**
 * Serialize a PaytoURI into a valid payto:// string
 * @deprecated use paytos namespace
 *
 * @param p
 * @returns
 */
export function stringifyPaytoUri(p: PaytoUri): PaytoString {
  const url = new URL(`${PAYTO_PREFIX}${p.targetType}/${p.targetPath}`);
  const paramList = !p.params ? [] : Object.entries(p.params);
  url.search = createSearchParams(paramList);
  return url.href as PaytoString;
}

/**
 * @deprecated use paytos namespace
 */
export function hashFullPaytoUri(p: PaytoUri | string): Uint8Array {
  const paytoUri = typeof p === "string" ? p : stringifyPaytoUri(p);
  return hashTruncate32(stringToBytes(paytoUri + "\0"));
}

/**
 * Normalize and then hash a payto URI.
 * @deprecated use paytos namespace
 */
export function hashNormalizedPaytoUri(p: PaytoUri | string): Uint8Array {
  const paytoUri = typeof p === "string" ? p : stringifyPaytoUri(p);
  if (typeof p === "string") {
    const parseRes = parsePaytoUri(p);
    if (!parseRes) {
      throw Error("invalid payto URI");
    }
    p = parseRes;
  }
  let paytoStr: string;
  if (!p.isKnown) {
    const normalizedPayto: PaytoUri = {
      targetType: p.targetType,
      targetPath: p.targetPath,
      isKnown: false,
      params: {},
    };
    paytoStr = stringifyPaytoUri(normalizedPayto);
  } else {
    switch (p.targetType) {
      case "iban":
        // FIXME: Strip BIC?
        paytoStr = `payto://iban/${p.targetPath}`;
        break;
      case "x-taler-bank":
        paytoStr = `payto://x-taler-bank/${p.host}/${p.account}`;
        break;
      case "bitcoin":
        paytoStr = `payto://bitcoin/${p.address}`;
        break;
      case "ethereum":
        paytoStr = `payto://ethereum/${p.address}`;
        break;
      case "taler-reserve":
        paytoStr = `payto://taler-reserve/${p.exchange}/${p.reservePub}`;
        break;
      case "taler-reserve-http":
        paytoStr = `payto://taler-reserve-http/${p.exchange}/${p.reservePub}`;
        break;
    }
  }
  return hashTruncate32(stringToBytes(paytoStr + "\0"));
}

/**
 * @deprecated do not use this, create a taler-reserve payto and use
 * stringify
 *
 * @param exchangeBaseUrl
 * @param reservePub
 * @returns
 */
export function stringifyReservePaytoUri(
  exchangeBaseUrl: string,
  reservePub: string,
): string {
  const url = new URL(exchangeBaseUrl);
  let target: string;
  let domainWithOptPort: string;
  if (url.protocol === "https:") {
    target = "taler-reserve";
    if (url.port != "443" && url.port !== "") {
      domainWithOptPort = `${url.hostname}:${url.port}`;
    } else {
      domainWithOptPort = `${url.hostname}`;
    }
  } else {
    target = "taler-reserve-http";
    if (url.port != "80" && url.port !== "") {
      domainWithOptPort = `${url.hostname}:${url.port}`;
    } else {
      domainWithOptPort = `${url.hostname}`;
    }
  }
  let optPath = "";
  if (url.pathname !== "/" && url.pathname !== "") {
    optPath = url.pathname;
  }
  return `payto://${target}/${domainWithOptPort}${optPath}/${reservePub}`;
}

/**
 * @deprecated use new Payto namespace functions
 *
 * @param s
 * @returns
 */
export function parsePaytoUriOrThrow(s: string): PaytoUri {
  const ret = parsePaytoUri(s);
  if (!ret) {
    throw Error("invalid payto URI");
  }
  return ret;
}

/**
 * Parse a valid payto:// uri into a PaytoUri object
 * RFC 8905
 * @deprecated use new Payto namespace functions
 *
 * @param s
 * @returns
 */
export function parsePaytoUri(s: string): PaytoUri | undefined {
  if (!s.startsWith(PAYTO_PREFIX)) {
    return undefined;
  }

  const [acct, search] = s.slice(PAYTO_PREFIX.length).split("?");

  const firstSlashPos = acct.indexOf("/");

  if (firstSlashPos === -1) {
    return undefined;
  }

  const targetType = acct.slice(0, firstSlashPos) as PaytoType;
  const targetPath = acct.slice(firstSlashPos + 1);

  const params: { [k: string]: string } = {};

  const searchParams = new URLSearchParams(search || "");

  searchParams.forEach((v, k) => {
    // URLSearchParams already decodes uri components
    params[k] = v; //decodeURIComponent(v);
  });

  switch (targetType) {
    case "iban": {
      const parts = targetPath.split("/");
      let iban: string | undefined = undefined;
      let bic: string | undefined = undefined;
      if (parts.length === 1) {
        iban = parts[0].toUpperCase();
      }
      if (parts.length === 2) {
        bic = parts[0];
        iban = parts[1].toUpperCase();
      } else {
        iban = targetPath.toUpperCase();
      }
      return {
        isKnown: true,
        targetPath,
        targetType,
        params,
        iban,
        bic,
      };
    }
    case "bitcoin": {
      const msg = /\b([A-Z0-9]{52})\b/.exec(params["message"]);
      const reserve = !msg ? params["subject"] : msg[0];
      const pubRes = !reserve ? undefined : Paytos.parseReservePub(reserve);
      const addr =
        !pubRes || pubRes.type === "fail"
          ? undefined
          : generateFakeSegwitAddress(pubRes.body, targetPath);
      const segwitAddrs = !addr || addr.type === "fail" ? [] : addr.body;

      const result: PaytoUriBitcoin = {
        isKnown: true,
        targetPath,
        targetType,
        address: targetPath,
        params,
        segwitAddrs,
      };

      return result;
    }
    case "x-taler-bank": {
      const parts = targetPath.split("/");
      const host = parts[0];
      const account = parts[1];
      return {
        targetPath,
        targetType,
        params,
        isKnown: true,
        host,
        account,
      };
    }
    case "taler-reserve": {
      const parts = targetPath.split("/");
      const exchange = parts[0];
      const reservePub = parts[1];
      return {
        targetPath,
        targetType,
        params,
        isKnown: true,
        exchange,
        reservePub,
      };
    }
    case "ethereum": {
      const result: PaytoUriEthereum = {
        isKnown: true,
        targetPath,
        targetType,
        address: targetPath,
        params,
      };

      return result;
    }
    default: {
      return {
        targetPath,
        targetType,
        params,
        isKnown: false,
      };
    }
  }
}

/**
 * @deprecated do not use this, create a payto object and use stringify
 *
 * @param exchangeBaseUrl
 * @param reservePub
 * @returns
 */
export function talerPaytoFromExchangeReserve(
  exchangeBaseUrl: string,
  reservePub: string,
): string {
  const url = new URL(exchangeBaseUrl);
  let proto: string;
  if (url.protocol === "http:") {
    proto = "taler-reserve-http";
  } else if (url.protocol === "https:") {
    proto = "taler-reserve";
  } else {
    throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
  }

  let path = url.pathname;
  if (!path.endsWith("/")) {
    path = path + "/";
  }

  return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
}
