/*
 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 { generateFakeSegwitAddress } from "./bitcoin.js";
import {
  Codec,
  Context,
  DecodingError,
  buildCodecForObject,
  codecForStringURL,
  renderContext,
} from "./codec.js";
import {
  AccessToken,
  bytesToString,
  codecForAccessToken,
  codecOptional,
  hashTruncate32,
  stringToBytes,
} from "./index.js";
import { URLSearchParams } from "./url.js";

export type PaytoUri =
  | PaytoUriUnknown
  | PaytoUriIBAN
  | PaytoUriTalerBank
  | PaytoUriBitcoin;

// declare const __payto_str: unique symbol;
// export type PaytoString = string & { [__payto_str]: true };
export type PaytoString = string;

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(paytoPfx)) {
        throw new DecodingError(
          `expected start with payto at ${renderContext(c)} but got "${x}"`,
        );
      }
      return x as PaytoString;
    },
  };
}

export interface PaytoUriGeneric {
  targetType: PaytoType | string;
  targetPath: string;
  params: { [name: string]: string };
}

export interface PaytoUriUnknown extends PaytoUriGeneric {
  isKnown: false;
}

export interface PaytoUriIBAN extends PaytoUriGeneric {
  isKnown: true;
  targetType: "iban";
  iban: string;
  bic?: string;
}

export interface PaytoUriTalerBank extends PaytoUriGeneric {
  isKnown: true;
  targetType: "x-taler-bank";
  host: string;
  account: string;
}

export interface PaytoUriBitcoin extends PaytoUriGeneric {
  isKnown: true;
  targetType: "bitcoin";
  address: string;
  segwitAddrs: Array<string>;
}

const paytoPfx = "payto://";

export type PaytoType = "iban" | "bitcoin" | "x-taler-bank";

export function buildPayto(
  type: "iban",
  iban: string,
  bic: string | undefined,
): PaytoUriIBAN;
export function buildPayto(
  type: "bitcoin",
  address: string,
  reserve: string | undefined,
): PaytoUriBitcoin;
export function buildPayto(
  type: "x-taler-bank",
  host: string,
  account: string,
): PaytoUriTalerBank;
export function buildPayto(
  type: PaytoType,
  first: string,
  second?: string,
): PaytoUriGeneric {
  switch (type) {
    case "bitcoin": {
      const uppercased = first.toUpperCase();
      const result: PaytoUriBitcoin = {
        isKnown: true,
        targetType: "bitcoin",
        targetPath: first,
        address: uppercased,
        params: {},
        segwitAddrs: !second ? [] : generateFakeSegwitAddress(second, first),
      };
      return result;
    }
    case "iban": {
      const uppercased = first.toUpperCase();
      const result: PaytoUriIBAN = {
        isKnown: true,
        targetType: "iban",
        iban: uppercased,
        params: {},
        targetPath: !second ? uppercased : `${second}/${uppercased}`,
      };
      return result;
    }
    case "x-taler-bank": {
      if (!second) throw Error("missing account for payto://x-taler-bank");
      const result: PaytoUriTalerBank = {
        isKnown: true,
        targetType: "x-taler-bank",
        host: first,
        account: second,
        params: {},
        targetPath: `${first}/${second}`,
      };
      return result;
    }
    default: {
      const unknownType: never = type;
      throw Error(`unknown payto:// type ${unknownType}`);
    }
  }
}

/**
 * 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(paytoPfx.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 paytoPfx + acct;
  }
  return paytoPfx + 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
 *
 * @param p
 * @returns
 */
export function stringifyPaytoUri(p: PaytoUri): PaytoString {
  const url = new URL(`${paytoPfx}${p.targetType}/${p.targetPath}`);
  const paramList = !p.params ? [] : Object.entries(p.params);
  url.search = createSearchParams(paramList);
  return url.href as PaytoString;
}

export function hashPaytoUri(p: PaytoUri): string {
  const paytoUri = stringifyPaytoUri(p);
  return bytesToString(hashTruncate32(stringToBytes(paytoUri + "\0")));
}

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

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

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

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

  const targetType = acct.slice(0, firstSlashPos);
  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);
  });

  if (targetType === "x-taler-bank") {
    const parts = targetPath.split("/");
    const host = parts[0];
    const account = parts[1];
    return {
      targetPath,
      targetType,
      params,
      isKnown: true,
      host,
      account,
    };
  }
  if (targetType === "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,
    };
  }
  if (targetType === "bitcoin") {
    const msg = /\b([A-Z0-9]{52})\b/.exec(params["message"]);
    const reserve = !msg ? params["subject"] : msg[0];
    const segwitAddrs = !reserve
      ? []
      : generateFakeSegwitAddress(reserve, targetPath);

    const uppercased = targetType.toUpperCase();
    const result: PaytoUriBitcoin = {
      isKnown: true,
      targetPath,
      targetType,
      address: uppercased,
      params,
      segwitAddrs,
    };

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

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

/**
 * The account letter is all the information
 * the merchant backend requires from the
 * bank account to check transfer.
 *
 */
export type AccountLetter = {
  accountURI: PaytoString;
  infoURL: string;
  accountToken?: AccessToken;
};

export const codecForAccountLetter = (): Codec<AccountLetter> =>
  buildCodecForObject<AccountLetter>()
    .property("infoURL", codecForStringURL(true))
    .property("accountURI", codecForPaytoString())
    .property("accountToken", codecOptional(codecForAccessToken()))
    .build("AccountLetter");
