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

import {
  AbsoluteTime,
  AmountString,
  PaytoString,
  TalerError,
  TalerErrorCode,
  TranslatedString,
} from "@gnu-taler/taler-util";
import {
  ErrorNotification,
  InternationalizationAPI,
  notify,
  notifyError,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";

const TALER_SCREEN_ID = 2;
/**
 * Validate (the number part of) an amount.  If needed,
 * replace comma with a dot.  Returns 'false' whenever
 * the input is invalid, the valid amount otherwise.
 */
const amountRegex = /^[0-9]+(.[0-9]+)?$/;
export function validateAmount(
  maybeAmount: string | undefined,
): string | undefined {
  if (!maybeAmount || !amountRegex.test(maybeAmount)) {
    return;
  }
  return maybeAmount;
}

/**
 * Extract IBAN from a Payto URI.
 */
export function getIbanFromPayto(url: string): string {
  const pathSplit = new URL(url).pathname.split("/");
  let lastIndex = pathSplit.length - 1;
  // Happens if the path ends with "/".
  if (pathSplit[lastIndex] === "") lastIndex--;
  const iban = pathSplit[lastIndex];
  return iban;
}

export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
  return Object.keys(obj).some(
    (k) => (obj as Record<string, T>)[k] !== undefined,
  )
    ? obj
    : undefined;
}

export type PartialButDefined<T> = {
  [P in keyof T]: T[P] | undefined;
};

/**
 * every non-map field can be undefined
 */
export type WithIntermediate<Type> = {
  [prop in keyof Type]: Type[prop] extends PaytoString
    ? Type[prop] | undefined
    : Type[prop] extends AmountString
      ? Type[prop] | undefined
      : Type[prop] extends TranslatedString
        ? Type[prop] | undefined
        : Type[prop] extends object
          ? WithIntermediate<Type[prop]>
          : Type[prop] | undefined;
};
export type RecursivePartial<Type> = {
  [P in keyof Type]?: Type[P] extends (infer U)[]
    ? RecursivePartial<U>[]
    : Type[P] extends object
      ? RecursivePartial<Type[P]>
      : Type[P];
};
export type ErrorMessageMappingFor<Type> = {
  [prop in keyof Type]+?: Exclude<Type[prop], undefined> extends PaytoString // enumerate known object
    ? TranslatedString
    : Exclude<Type[prop], undefined> extends AmountString
      ? TranslatedString
      : Exclude<Type[prop], undefined> extends TranslatedString
        ? TranslatedString
        : // arrays: every element
          Exclude<Type[prop], undefined> extends (infer U)[]
          ? ErrorMessageMappingFor<U>[]
          : // map: every field
            Exclude<Type[prop], undefined> extends object
            ? ErrorMessageMappingFor<Type[prop]>
            : TranslatedString;
};

export enum TanChannel {
  SMS = "sms",
  EMAIL = "email",
}
export enum CashoutStatus {
  // The payment was initiated after a valid
  // TAN was received by the bank.
  CONFIRMED = "confirmed",

  // The cashout was created and now waits
  // for the TAN by the author.
  PENDING = "pending",
}

export const PAGINATED_LIST_SIZE = 5;
// when doing paginated request, ask for one more
// and use it to know if there are more to request
export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;

type Translator = ReturnType<typeof useTranslationContext>["i18n"];

export async function withRuntimeErrorHandling<T>(
  i18n: Translator,
  cb: () => Promise<T>,
): Promise<void> {
  try {
    await cb();
  } catch (error) {
    if (error instanceof TalerError) {
      notify(buildRequestErrorMessage(i18n, error));
    } else {
      notifyError(
        i18n.str`Operation failed, please report`,
        (error instanceof Error
          ? error.message
          : (error)) as TranslatedString,
      );
    }
  }
}

export function buildRequestErrorMessage(
  i18n: Translator,
  cause: TalerError,
): ErrorNotification {
  let result: ErrorNotification;
  switch (cause.errorDetail.code) {
    case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
      result = {
        type: "error",
        title: i18n.str`Request timeout`,
        description: cause.message as TranslatedString,
        debug: cause.errorDetail,
        when: AbsoluteTime.now(),
      };
      break;
    }
    case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
      result = {
        type: "error",
        title: i18n.str`Request throttled`,
        description: cause.message as TranslatedString,
        debug: cause.errorDetail,
        when: AbsoluteTime.now(),
      };
      break;
    }
    case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
      result = {
        type: "error",
        title: i18n.str`Malformed response`,
        description: cause.message as TranslatedString,
        debug: cause.errorDetail,
        when: AbsoluteTime.now(),
      };
      break;
    }
    case TalerErrorCode.WALLET_NETWORK_ERROR: {
      result = {
        type: "error",
        title: i18n.str`Network error`,
        description: cause.message as TranslatedString,
        debug: cause.errorDetail,
        when: AbsoluteTime.now(),
      };
      break;
    }
    case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
      result = {
        type: "error",
        title: i18n.str`Unexpected request error`,
        description: cause.message as TranslatedString,
        debug: cause.errorDetail,
        when: AbsoluteTime.now(),
      };
      break;
    }
    default: {
      result = {
        type: "error",
        title: i18n.str`Unexpected error`,
        description: cause.message as TranslatedString,
        debug: cause.errorDetail,
        when: AbsoluteTime.now(),
      };
      break;
    }
  }
  return result;
}

export const COUNTRY_TABLE = {
  AE: "U.A.E.",
  AF: "Afghanistan",
  AL: "Albania",
  AM: "Armenia",
  AN: "Netherlands Antilles",
  AR: "Argentina",
  AT: "Austria",
  AU: "Australia",
  AZ: "Azerbaijan",
  BA: "Bosnia and Herzegovina",
  BD: "Bangladesh",
  BE: "Belgium",
  BG: "Bulgaria",
  BH: "Bahrain",
  BN: "Brunei Darussalam",
  BO: "Bolivia",
  BR: "Brazil",
  BT: "Bhutan",
  BY: "Belarus",
  BZ: "Belize",
  CA: "Canada",
  CG: "Congo",
  CH: "Switzerland",
  CI: "Cote d'Ivoire",
  CL: "Chile",
  CM: "Cameroon",
  CN: "People's Republic of China",
  CO: "Colombia",
  CR: "Costa Rica",
  CS: "Serbia and Montenegro",
  CZ: "Czech Republic",
  DE: "Germany",
  DK: "Denmark",
  DO: "Dominican Republic",
  DZ: "Algeria",
  EC: "Ecuador",
  EE: "Estonia",
  EG: "Egypt",
  ER: "Eritrea",
  ES: "Spain",
  ET: "Ethiopia",
  FI: "Finland",
  FO: "Faroe Islands",
  FR: "France",
  GB: "United Kingdom",
  GD: "Caribbean",
  GE: "Georgia",
  GL: "Greenland",
  GR: "Greece",
  GT: "Guatemala",
  HK: "Hong Kong",
  // HK: "Hong Kong S.A.R.",
  HN: "Honduras",
  HR: "Croatia",
  HT: "Haiti",
  HU: "Hungary",
  ID: "Indonesia",
  IE: "Ireland",
  IL: "Israel",
  IN: "India",
  IQ: "Iraq",
  IR: "Iran",
  IS: "Iceland",
  IT: "Italy",
  JM: "Jamaica",
  JO: "Jordan",
  JP: "Japan",
  KE: "Kenya",
  KG: "Kyrgyzstan",
  KH: "Cambodia",
  KR: "South Korea",
  KW: "Kuwait",
  KZ: "Kazakhstan",
  LA: "Laos",
  LB: "Lebanon",
  LI: "Liechtenstein",
  LK: "Sri Lanka",
  LT: "Lithuania",
  LU: "Luxembourg",
  LV: "Latvia",
  LY: "Libya",
  MA: "Morocco",
  MC: "Principality of Monaco",
  MD: "Moldava",
  // MD: "Moldova",
  ME: "Montenegro",
  MK: "Former Yugoslav Republic of Macedonia",
  ML: "Mali",
  MM: "Myanmar",
  MN: "Mongolia",
  MO: "Macau S.A.R.",
  MT: "Malta",
  MV: "Maldives",
  MX: "Mexico",
  MY: "Malaysia",
  NG: "Nigeria",
  NI: "Nicaragua",
  NL: "Netherlands",
  NO: "Norway",
  NP: "Nepal",
  NZ: "New Zealand",
  OM: "Oman",
  PA: "Panama",
  PE: "Peru",
  PH: "Philippines",
  PK: "Islamic Republic of Pakistan",
  PL: "Poland",
  PR: "Puerto Rico",
  PT: "Portugal",
  PY: "Paraguay",
  QA: "Qatar",
  RE: "Reunion",
  RO: "Romania",
  RS: "Serbia",
  RU: "Russia",
  RW: "Rwanda",
  SA: "Saudi Arabia",
  SE: "Sweden",
  SG: "Singapore",
  SI: "Slovenia",
  SK: "Slovak",
  SN: "Senegal",
  SO: "Somalia",
  SR: "Suriname",
  SV: "El Salvador",
  SY: "Syria",
  TH: "Thailand",
  TJ: "Tajikistan",
  TM: "Turkmenistan",
  TN: "Tunisia",
  TR: "Turkey",
  TT: "Trinidad and Tobago",
  TW: "Taiwan",
  TZ: "Tanzania",
  UA: "Ukraine",
  US: "United States",
  UY: "Uruguay",
  VA: "Vatican",
  VE: "Venezuela",
  VN: "Viet Nam",
  YE: "Yemen",
  ZA: "South Africa",
  ZW: "Zimbabwe",
};

/**
 * 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.
 *
 */
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
export function validateIBAN(
  account: string,
  i18n: InternationalizationAPI,
): TranslatedString | undefined {
  if (!IBAN_REGEX.test(account)) {
    return i18n.str`An IBAN consists of capital letters and numbers only`;
  }
  // Check total length
  if (account.length < 4) return i18n.str`IBAN numbers have more that 4 digits`;
  if (account.length > 34)
    return i18n.str`IBAN numbers have less that 34 digits`;

  const A_code = "A".charCodeAt(0);
  const Z_code = "Z".charCodeAt(0);
  const IBAN = account.toUpperCase();
  // check supported country
  const code = IBAN.substring(0, 2);
  const found = code in COUNTRY_TABLE;
  if (!found) return i18n.str`IBAN country code not found`;

  // 2.- Move the four initial characters to the end of the string
  const step2 = IBAN.substring(4) + account.substring(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("");

  const checksum = calculate_iban_checksum(step3);
  if (checksum !== 1)
    return i18n.str`IBAN number is not valid, checksum is wrong`;
  return undefined;
}

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

export const USERNAME_REGEX = /^[a-zA-Z0-9\-\.\_\~]*$/;
// [a-zA-Z0-9\\-\\._~]+
export function validateTalerBank(
  account: string,
  i18n: InternationalizationAPI,
): TranslatedString | undefined {
  if (!USERNAME_REGEX.test(account)) {
    return i18n.str`Use letters, numbers or any of these characters: - . _ ~`;
  }
  return undefined;
}
