/*
 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 <http://www.gnu.org/licenses/>
 */

/**
 * Client for the Taler (demo-)bank.
 */

/**
 * Imports.
 */
import {
  AmountString,
  base64FromArrayBuffer,
  buildCodecForObject,
  Codec,
  codecForAny,
  codecForString,
  encodeCrock,
  getRandomBytes,
  HttpStatusCode,
  j2s,
  Logger,
  opEmptySuccess,
  opKnownHttpFailure,
  opUnknownHttpFailure,
  PaytoString,
  stringToBytes,
  TalerCorebankApi,
  TalerError,
  TalerErrorCode,
} from "@gnu-taler/taler-util";
import {
  createPlatformHttpLib,
  expectSuccessResponseOrThrow,
  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: PaytoString;
}

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<WithdrawalOperationInfo> =>
  buildCodecForObject<WithdrawalOperationInfo>()
    .property("withdrawal_id", codecForString())
    .property("taler_withdraw_uri", codecForString())
    .build("WithdrawalOperationInfo");

export interface BankAccessApiClientArgs {
  auth?: { username: string; password: string };
  httpClient?: HttpRequestLibrary;
}

export interface AccountBalance {
  amount: AmountString;
  credit_debit_indicator: "credit" | "debit";
}

export interface ConfirmWithdrawalArgs {
  withdrawalOperationId: string;
}

/**
 * Client for the Taler corebank API.
 *
 * @deprecated use TalerCoreBankHttpClient instead
 */
export class TalerCorebankApiClient {
  httpLib: HttpRequestLibrary;

  constructor(
    public baseUrl: string,
    private args: BankAccessApiClientArgs = {},
  ) {
    this.httpLib = args.httpClient ?? createPlatformHttpLib();
  }

  setAuth(auth: { username: string; password: string }) {
    this.args.auth = auth;
  }

  private makeAuthHeader(): Record<string, string> {
    if (!this.args.auth) {
      return {};
    }
    const authHeaderValue = makeBasicAuthHeader(
      this.args.auth.username,
      this.args.auth.password,
    );
    return {
      Authorization: authHeaderValue,
    };
  }

  async getAccountBalance(
    username: string,
  ): Promise<BankAccountBalanceResponse> {
    const url = new URL(`accounts/${username}`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      headers: this.makeAuthHeader(),
    });
    return readSuccessResponseJsonOrThrow(resp, codecForAny());
  }

  async makeTransaction(
    amount: AmountString,
    target: PaytoString,
  ): Promise<void> {
    const reqUrl = new URL(
      `accounts/${this.args.auth!.username}/transactions`,
      this.baseUrl,
    );
    const resp = await this.httpLib.fetch(reqUrl.href, {
      method: "POST",
      body: {
        amount,
        payto_uri: target,
      },
      headers: {
        ...this.makeAuthHeader(),
        "Content-Type": "application/json",
      },
    });

    const res = await readSuccessResponseJsonOrThrow(resp, codecForAny());
    logger.info(`result: ${j2s(res)}`);
  }

  async getTransactions(username: string): Promise<void> {
    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 registerAccountExtended(
    req: TalerCorebankApi.RegisterAccountRequest,
  ): Promise<void> {
    const url = new URL("accounts", this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: req,
      headers: this.makeAuthHeader(),
    });

    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<BankUser> {
    const url = new URL("accounts", this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: {
        username,
        password,
        name: username,
      },
      headers: this.makeAuthHeader(),
    });
    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: TalerCorebankApi.AccountData =
      await readSuccessResponseJsonOrThrow(infoResp, codecForAny());
    return {
      password,
      username,
      accountPaytoUri: acctInfo.payto_uri,
    };
  }

  async createRandomBankUser(): Promise<BankUser> {
    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 | undefined,
  ): Promise<WithdrawalOperationInfo> {
    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,
  ) {
    const url = new URL(
      `accounts/${username}/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(`confirm response status ${resp.status}`);

    switch (resp.status) {
      case HttpStatusCode.Ok:
      case HttpStatusCode.NoContent:
        return opEmptySuccess();
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  async abortWithdrawalOperation(wopi: WithdrawalOperationInfo): Promise<void> {
    const url = new URL(
      `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());
  }

  async abortWithdrawalOperationV2(
    username: string,
    wopi: WithdrawalOperationInfo,
  ): Promise<void> {
    const url = new URL(
      `accounts/${username}/withdrawals/${wopi.withdrawal_id}/abort`,
      this.baseUrl,
    );
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: {},
      headers: this.makeAuthHeader(),
    });
    await expectSuccessResponseOrThrow(resp);
  }
}
