/*
 This file is part of GNU Taler
 (C) 2022 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 
 */
/**
 * Client for the Taler (demo-)bank.
 */
/**
 * Imports.
 */
import {
  AmountString,
  base64FromArrayBuffer,
  buildCodecForObject,
  Codec,
  codecForAny,
  codecForString,
  encodeCrock,
  generateIban,
  getRandomBytes,
  j2s,
  Logger,
  stringToBytes,
  TalerError,
  TalerErrorCode,
} from "@gnu-taler/taler-util";
import {
  checkSuccessResponseOrThrow,
  createPlatformHttpLib,
  HttpRequestLibrary,
  readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
const logger = new Logger("bank-api-client.ts");
export enum CreditDebitIndicator {
  Credit = "credit",
  Debit = "debit",
}
export interface BankAccountBalanceResponse {
  balance: {
    amount: AmountString;
    credit_debit_indicator: CreditDebitIndicator;
  };
}
export interface BankUser {
  username: string;
  password: string;
  accountPaytoUri: string;
}
export interface WithdrawalOperationInfo {
  withdrawal_id: string;
  taler_withdraw_uri: string;
}
/**
 * Helper function to generate the "Authorization" HTTP header.
 */
function makeBasicAuthHeader(username: string, password: string): string {
  const auth = `${username}:${password}`;
  const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth));
  return `Basic ${authEncoded}`;
}
const codecForWithdrawalOperationInfo = (): Codec =>
  buildCodecForObject()
    .property("withdrawal_id", codecForString())
    .property("taler_withdraw_uri", codecForString())
    .build("WithdrawalOperationInfo");
export interface BankAccessApiClientArgs {
  auth?: { username: string; password: string };
  httpClient?: HttpRequestLibrary;
}
export interface BankAccessApiCreateTransactionRequest {
  amount: AmountString;
  paytoUri: string;
}
export class WireGatewayApiClientArgs {
  auth?: {
    username: string;
    password: string;
  };
  httpClient?: HttpRequestLibrary;
}
/**
 * This API look like it belongs to harness
 * but it will be nice to have in utils to be used by others
 */
export class WireGatewayApiClient {
  httpLib;
  constructor(
    private baseUrl: string,
    private args: WireGatewayApiClientArgs = {},
  ) {
    this.httpLib = args.httpClient ?? createPlatformHttpLib();
  }
  private makeAuthHeader(): Record {
    const auth = this.args.auth;
    if (auth) {
      return {
        Authorization: makeBasicAuthHeader(auth.username, auth.password),
      };
    }
    return {};
  }
  async adminAddIncoming(params: {
    amount: string;
    reservePub: string;
    debitAccountPayto: string;
  }): Promise {
    let url = new URL(`admin/add-incoming`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: {
        amount: params.amount,
        reserve_pub: params.reservePub,
        debit_account: params.debitAccountPayto,
      },
      headers: this.makeAuthHeader(),
    });
    logger.info(`add-incoming response status: ${resp.status}`);
    await checkSuccessResponseOrThrow(resp);
  }
}
export interface ChallengeContactData {
  // E-Mail address
  email?: string;
  // Phone number.
  phone?: string;
}
export interface AccountBalance {
  amount: AmountString;
  credit_debit_indicator: "credit" | "debit";
}
export interface RegisterAccountRequest {
  // Username
  username: string;
  // Password.
  password: string;
  // Legal name of the account owner
  name: string;
  // Defaults to false.
  is_public?: boolean;
  // Is this a taler exchange account?
  // If true:
  // - incoming transactions to the account that do not
  //   have a valid reserve public key are automatically
  // - the account provides the taler-wire-gateway-api endpoints
  // Defaults to false.
  is_taler_exchange?: boolean;
  // Addresses where to send the TAN for transactions.
  // Currently only used for cashouts.
  // If missing, cashouts will fail.
  // In the future, might be used for other transactions
  // as well.
  challenge_contact_data?: ChallengeContactData;
  // 'payto' address pointing a bank account
  // external to the libeufin-bank.
  // Payments will be sent to this bank account
  // when the user wants to convert the local currency
  // back to fiat currency outside libeufin-bank.
  cashout_payto_uri?: string;
  // Internal payto URI of this bank account.
  // Used mostly for testing.
  internal_payto_uri?: string;
}
export interface AccountData {
  // Legal name of the account owner.
  name: string;
  // Available balance on the account.
  balance: AccountBalance;
  // payto://-URI of the account.
  payto_uri: string;
  // Number indicating the max debit allowed for the requesting user.
  debit_threshold: AmountString;
  contact_data?: ChallengeContactData;
  // 'payto' address pointing the bank account
  // where to send cashouts.  This field is optional
  // because not all the accounts are required to participate
  // in the merchants' circuit.  One example is the exchange:
  // that never cashouts.  Registering these accounts can
  // be done via the access API.
  cashout_payto_uri?: string;
}
export interface ConfirmWithdrawalArgs {
  withdrawalOperationId: string;
}
/**
 * Client for the Taler corebank API.
 */
export class TalerCorebankApiClient {
  httpLib: HttpRequestLibrary;
  constructor(
    private baseUrl: string,
    private args: BankAccessApiClientArgs = {},
  ) {
    this.httpLib = args.httpClient ?? createPlatformHttpLib();
  }
  setAuth(auth: { username: string; password: string }) {
    this.args.auth = auth;
  }
  private makeAuthHeader(): Record {
    if (!this.args.auth) {
      return {};
    }
    const authHeaderValue = makeBasicAuthHeader(
      this.args.auth.username,
      this.args.auth.password,
    );
    return {
      Authorization: authHeaderValue,
    };
  }
  async getAccountBalance(
    username: string,
  ): Promise {
    const url = new URL(`accounts/${username}`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      headers: this.makeAuthHeader(),
    });
    return readSuccessResponseJsonOrThrow(resp, codecForAny());
  }
  async getTransactions(username: string): Promise {
    const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl);
    const resp = await this.httpLib.fetch(reqUrl.href, {
      method: "GET",
      headers: {
        ...this.makeAuthHeader(),
      },
    });
    const res = await readSuccessResponseJsonOrThrow(resp, codecForAny());
    logger.info(`result: ${j2s(res)}`);
  }
  async createTransaction(
    username: string,
    req: BankAccessApiCreateTransactionRequest,
  ): Promise {
    const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl);
    const resp = await this.httpLib.fetch(reqUrl.href, {
      method: "POST",
      body: req,
      headers: this.makeAuthHeader(),
    });
    return await readSuccessResponseJsonOrThrow(resp, codecForAny());
  }
  async registerAccountExtended(req: RegisterAccountRequest): Promise {
    const url = new URL("accounts", this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: req,
    });
    if (
      resp.status !== 200 &&
      resp.status !== 201 &&
      resp.status !== 202 &&
      resp.status !== 204
    ) {
      logger.error(`unexpected status ${resp.status} from POST ${url.href}`);
      logger.error(`${j2s(await resp.json())}`);
      throw TalerError.fromDetail(
        TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
        {
          httpStatusCode: resp.status,
        },
      );
    }
  }
  /**
   * Register a new account and return information about it.
   *
   * This is a helper, as it does both the registration and the
   * account info query.
   */
  async registerAccount(username: string, password: string): Promise {
    const url = new URL("accounts", this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: {
        username,
        password,
        name: username,
      },
    });
    if (
      resp.status !== 200 &&
      resp.status !== 201 &&
      resp.status !== 202 &&
      resp.status !== 204
    ) {
      logger.error(`unexpected status ${resp.status} from POST ${url.href}`);
      logger.error(`${j2s(await resp.json())}`);
      throw TalerError.fromDetail(
        TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
        {
          httpStatusCode: resp.status,
        },
      );
    }
    // FIXME: Corebank should directly return this info!
    const infoUrl = new URL(`accounts/${username}`, this.baseUrl);
    const infoResp = await this.httpLib.fetch(infoUrl.href, {
      headers: {
        Authorization: makeBasicAuthHeader(username, password),
      },
    });
    // FIXME: Validate!
    const acctInfo: AccountData = await readSuccessResponseJsonOrThrow(
      infoResp,
      codecForAny(),
    );
    return {
      password,
      username,
      accountPaytoUri: acctInfo.payto_uri,
    };
  }
  async createRandomBankUser(): Promise {
    const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
    const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
    return await this.registerAccount(username, password);
  }
  async createWithdrawalOperation(
    user: string,
    amount: string,
  ): Promise {
    const url = new URL(`accounts/${user}/withdrawals`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: {
        amount,
      },
      headers: this.makeAuthHeader(),
    });
    return readSuccessResponseJsonOrThrow(
      resp,
      codecForWithdrawalOperationInfo(),
    );
  }
  async confirmWithdrawalOperation(
    username: string,
    wopi: ConfirmWithdrawalArgs,
  ): Promise {
    const url = new URL(
      `withdrawals/${wopi.withdrawalOperationId}/confirm`,
      this.baseUrl,
    );
    logger.info(`confirming withdrawal operation via ${url.href}`);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: {},
      headers: this.makeAuthHeader(),
    });
    logger.info(`response status ${resp.status}`);
    const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny());
    // FIXME: We don't check the status here!
  }
  async abortWithdrawalOperation(
    accountName: string,
    wopi: WithdrawalOperationInfo,
  ): Promise {
    const url = new URL(
      `accounts/${accountName}/withdrawals/${wopi.withdrawal_id}/abort`,
      this.baseUrl,
    );
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: {},
      headers: this.makeAuthHeader(),
    });
    await readSuccessResponseJsonOrThrow(resp, codecForAny());
  }
}