/*
 This file is part of GNU Taler
 (C) 2021-2024 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/>
 */

/**
 *
 * @author Sebastian Javier Marchano (sebasjm)
 */
import {
  InternationalizationAPI,
  PaytoUri,
  TranslatedString,
  parsePaytoUri,
  stringifyPaytoUri,
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { useSessionContext } from "../../context/session.js";
import { COUNTRY_TABLE } from "../../utils/constants.js";
import { undefinedIfEmpty } from "../../utils/table.js";
import { FormErrors, FormProvider, TalerForm } from "./FormProvider.js";
import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js";


const TALER_SCREEN_ID = 11;

export interface Props<T> extends InputProps<T> { }

// type Entity = PaytoUriGeneric
// https://datatracker.ietf.org/doc/html/rfc8905
type Entity = {
  // iban, bitcoin, x-taler-bank. it defined the format
  target: string | undefined;
  // path1 if the first field to be used
  path1?: string;
  // path2 if the second field to be used, optional
  path2?: string;
  // params of the payto uri
  params: {
    "receiver-name"?: string;
    sender?: string;
    message?: string;
    amount?: string;
    instruction?: string;
    [name: string]: string | undefined;
  } & TalerForm;
};

function isEthereumAddress(address: string) {
  if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
    return false;
  } else if (
    /^(0x|0X)?[0-9a-f]{40}$/.test(address) ||
    /^(0x|0X)?[0-9A-F]{40}$/.test(address)
  ) {
    return true;
  }
  return checkAddressChecksum(address);
}

function checkAddressChecksum(_address: string) {
  //TODO implement ethereum checksum
  return true;
}

function validateBitcoin_path1(
  addr: string,
  i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString | undefined {
  try {
    const valid = /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/.test(addr);
    if (valid) return undefined;
  } catch (e) {
    console.log(e);
  }
  return i18n.str`This is not a valid Bitcoin address.`;
}

function validateEthereum_path1(
  addr: string,
  i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString | undefined {
  try {
    const valid = isEthereumAddress(addr);
    if (valid) return undefined;
  } catch (e) {
    console.log(e);
  }
  return i18n.str`This is not a valid Ethereum address.`;
}

/**
 * validates "[host]:[port]/[path]/" where:
 * host: can be localhost, bank.com
 * port: any number
 * path: may include subpath
 *
 * for example
 * localhost
 * bank.com/
 * bank.com
 * bank.com/path
 * bank.com/path/subpath/
 */
const DOMAIN_REGEX =
  /^[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9-_](?:\.[a-zA-Z0-9-_]{2,})*(:[0-9]+)?(\/[a-zA-Z0-9-.]+)*\/?$/;

function validateTalerBank_path1(
  addr: string,
  i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString | undefined {
  try {
    const valid = DOMAIN_REGEX.test(addr);
    if (valid) return undefined;
  } catch (e) {
    console.log(e);
  }
  return i18n.str`This is not a valid host.`;
}

/**
 * An IBAN is validated by converting it into an integer and performing a
 * basic mod-97 operation (as described in ISO 7064) on it.
 * If the IBAN is valid, the remainder equals 1.
 *
 * The algorithm of IBAN validation is as follows:
 * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid
 * 2.- Move the four initial characters to the end of the string
 * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35
 * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97
 *
 * If the remainder is 1, the check digit test is passed and the IBAN might be valid.
 *
 */
function validateIBAN_path1(
  iban: string,
  i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString | undefined {
  // Check total length
  if (iban.length < 4) return i18n.str`IBANs usually have more than 4 digits.`;
  if (iban.length > 34)
    return i18n.str`IBANs usually have fewer than 34 digits.`;

  const A_code = "A".charCodeAt(0);
  const Z_code = "Z".charCodeAt(0);
  const IBAN = iban.toUpperCase();
  // check supported country
  const code = IBAN.substr(0, 2);
  const found = code in COUNTRY_TABLE;
  if (!found) return i18n.str`The IBAN's country code could not be retrieved.`;

  // 2.- Move the four initial characters to the end of the string
  const step2 = IBAN.substr(4) + iban.substr(0, 4);
  const step3 = Array.from(step2)
    .map((letter) => {
      const code = letter.charCodeAt(0);
      if (code < A_code || code > Z_code) return letter;
      return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`;
    })
    .join("");

  function calculate_iban_checksum(str: string): number {
    const numberStr = str.substr(0, 5);
    const rest = str.substr(5);
    const number = parseInt(numberStr, 10);
    const result = number % 97;
    if (rest.length > 0) {
      return calculate_iban_checksum(`${result}${rest}`);
    }
    return result;
  }

  const checksum = calculate_iban_checksum(step3);
  if (checksum !== 1)
    return i18n.str`The IBAN is invalid because the checksum is wrong.`;
  return undefined;
}

const allTargets = ["iban", "bitcoin", "ethereum", "x-taler-bank"];

function checkServerRegex(i18n: InternationalizationAPI, payto: string | undefined, serverReg: string | undefined) {
  if (!payto || !serverReg) return undefined
  try {
    const validator = new RegExp(serverReg)
    if (!validator.test(payto)) {
      return i18n.str`This account is not allowed.`
    } else {
      return undefined
    }
  } catch (e) {
    return undefined
  }
}

export function InputPaytoForm<T>({
  name,
  readonly,
  label,
  tooltip,
}: Props<keyof T>): VNode {
  const { value: initialValueStr, onChange } = useField<T>(name);

  const { config } = useSessionContext();

  const initialPayto = parsePaytoUri(initialValueStr ?? "");
  const { i18n } = useTranslationContext();

  const supportedWireMethods =
    config.payment_target_types === "*"
      ? undefined
      : config.payment_target_types.trim().split(" ");

  // only the one supported by the server
  const targets =
    !supportedWireMethods || !supportedWireMethods.length
      ? allTargets
      : allTargets.filter((t) => supportedWireMethods.indexOf(t) !== -1);

  const defaultTarget: Entity = {
    target: targets.length ? targets[0] : "",
    params: {},
  };

  // FIXME: use new paytos API and EBICS is not supported
  const paths = !initialPayto ? [] : initialPayto.targetPath.split("/");
  const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
  const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
  const initial: Entity =
    initialPayto === undefined
      ? defaultTarget
      : {
        target: initialPayto.targetType,
        params: initialPayto.params,
        path1: initialPath1,
        path2: initialPath2,
      };

  const [value, setValue] = useState<Partial<Entity>>(initial);

  const errors = undefinedIfEmpty<FormErrors<Entity>>({
    target: value.target === undefined ? i18n.str`Required` : undefined,
    path1: !value.path1
      ? i18n.str`Required`
      : value.target === "iban"
        ? validateIBAN_path1(cleanupPath1(value.path1, value.target), i18n)
        : value.target === "bitcoin"
          ? validateBitcoin_path1(cleanupPath1(value.path1, value.target), i18n)
          : value.target === "ethereum"
            ? validateEthereum_path1(
              cleanupPath1(value.path1, value.target),
              i18n,
            )
            : value.target === "x-taler-bank"
              ? validateTalerBank_path1(
                cleanupPath1(value.path1, value.target),
                i18n,
              )
              : undefined,
    path2:
      value.target === "x-taler-bank"
        ? !value.path2
          ? i18n.str`Required`
          : undefined
        : undefined,
    params: undefinedIfEmpty({
      "receiver-name": !value.params?.["receiver-name"]
        ? i18n.str`Required`
        : undefined,
    }),
  });

  const hasErrors = errors !== undefined;

  const path1WithSlash = !value.path1
    ? undefined
    : cleanupPath1(value.path1, value.target);
  const pto =
    hasErrors || !value.target
      ? undefined
      : ({
        targetType: value.target,
        targetPath: value.path2
          ? `${path1WithSlash}${value.path2}`
          : path1WithSlash ?? "",
        params: value.params ?? {},
        isKnown: false as const,
      } as PaytoUri);

  const str = !pto ? undefined : stringifyPaytoUri(pto);

  const regexError = checkServerRegex(i18n, str, config.payment_target_regex)

  useEffect(() => {
    onChange(str as T[keyof T]);
  }, [str]);

  if (!targets.length) {
    return (
      <i18n.Translate>
        None of the supported wire method of the server are currently supported
        by this app. Server settings: {supportedWireMethods?.join(",") ?? "-"}
      </i18n.Translate>
    );
  }
  return (
    <InputGroup name="payto" label={label} fixed tooltip={tooltip}>
      <FormProvider<Entity>
        name="tax"
        errors={regexError  ? {path1: regexError} : errors}
        object={value}
        valueHandler={setValue}
      >
        {targets.length === 1 ? undefined : (
          <InputSelector<Entity>
            name="target"
            label={i18n.str`Wire method`}
            tooltip={i18n.str`Select the method you want to use to transfer your earnings to your business account.`}
            values={targets}
            readonly={readonly}
            toStr={(v) =>
              v === undefined ? i18n.str`Select a wire method...` : v
            }
          />
        )}

        {value.target === "ach" && (
          <Fragment>
            <Input<Entity>
              name="path1"
              label={i18n.str`Routing`}
              readonly={readonly}
              tooltip={i18n.str`Routing number`}
            />
            <Input<Entity>
              name="path2"
              label={i18n.str`Account`}
              readonly={readonly}
              tooltip={i18n.str`Account number`}
            />
          </Fragment>
        )}
        {value.target === "bic" && (
          <Fragment>
            <Input<Entity>
              name="path1"
              label={i18n.str`Code`}
              readonly={readonly}
              tooltip={i18n.str`Business Identifier Code`}
            />
          </Fragment>
        )}
        {value.target === "iban" && (
          <Fragment>
            <Input<Entity>
              name="path1"
              label={i18n.str`IBAN`}
              tooltip={i18n.str`International Bank Account Number`}
              readonly={readonly}
              placeholder="e.g. DE12 0000 1111 2222 3333 00"
              inputExtra={{ style: { textTransform: "uppercase" } }}
            />
          </Fragment>
        )}
        {value.target === "upi" && (
          <Fragment>
            <Input<Entity>
              name="path1"
              readonly={readonly}
              label={i18n.str`Account`}
              tooltip={i18n.str`Unified Payment Interface`}
            />
          </Fragment>
        )}
        {value.target === "bitcoin" && (
          <Fragment>
            <Input<Entity>
              name="path1"
              readonly={readonly}
              label={i18n.str`Address`}
              tooltip={i18n.str`Bitcoin protocol`}
            />
          </Fragment>
        )}
        {value.target === "ethereum" && (
          <Fragment>
            <Input<Entity>
              name="path1"
              readonly={readonly}
              label={i18n.str`Address`}
              tooltip={i18n.str`Ethereum protocol`}
            />
          </Fragment>
        )}
        {value.target === "ilp" && (
          <Fragment>
            <Input<Entity>
              name="path1"
              readonly={readonly}
              label={i18n.str`Address`}
              tooltip={i18n.str`Interledger protocol`}
            />
          </Fragment>
        )}
        {value.target === "void" && <Fragment />}
        {value.target === "x-taler-bank" && (
          <Fragment>
            <Input<Entity>
              name="path1"
              readonly={readonly}
              label={i18n.str`Host`}
              fromStr={(v) => {
                if (v.startsWith("http")) {
                  try {
                    const url = new URL(v);
                    return url.host + url.pathname;
                  } catch {
                    return v;
                  }
                }
                return v;
              }}
              tooltip={i18n.str`Bank host`}
              help={
                <Fragment>
                  <div>
                    <i18n.Translate>
                      Enter the data without a scheme. A subpath may be
                      included:
                    </i18n.Translate>
                  </div>
                  <div>bank.com/</div>
                  <div>bank.com/path/subpath/</div>
                </Fragment>
              }
            />
            <Input<Entity>
              name="path2"
              readonly={readonly}
              label={i18n.str`Account`}
              tooltip={i18n.str`Bank account`}
            />
          </Fragment>
        )}

        {/**
         * Show additional fields apart from the payto
         */}
        {value.target !== undefined && (
          <Fragment>
            <Input
              name="params.receiver-name"
              readonly={readonly}
              label={i18n.str`Name of the account holder`}
              placeholder="John Doe"
              tooltip={i18n.str`Legal name of the person holding the account.`}
            />
          </Fragment>
        )}
      </FormProvider>
    </InputGroup>
  );
}
function cleanupPath1(str: string, type?: string) {
  if (type === "iban") {
    // we tolerate country in any case
    // and don't care about any space
    return str.replace(/\s/g, "").toUpperCase();
  }
  if (type === "x-taler-bank") {
    return !str.endsWith("/") ? str + "/" : str;
  }
  return str;
}
