/*
 This file is part of GNU Taler
 (C) 2022-2025 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 { codecForAny } from "../codec.js";
import {
  HttpRequestLibrary,
  HttpRequestOptions,
  HttpResponse,
  readSuccessResponseJsonOrThrow,
} from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import { LibtoolVersion } from "../libtool-version.js";
import {
  FailCasesByMethod,
  OperationAlternative,
  OperationFail,
  OperationOk,
  ResultByMethod,
  carefullyParseConfig,
  opEmptySuccess,
  opFixedSuccess,
  opKnownAlternativeHttpFailure,
  opKnownFailure,
  opKnownHttpFailure,
  opSuccessFromHttp,
  opUnknownHttpFailure,
} from "../operation.js";
import { encodeCrock } from "../taler-crypto.js";
import {
  AccessToken,
  EddsaPublicKeyString,
  EddsaSignatureString,
  LongPollParams,
  OfficerAccount,
  PaginationParams,
} from "../types-taler-common.js";
import {
  AccountKycStatus,
  AmlDecisionRequest,
  AmlDecisionsResponse,
  AvailableMeasureSummary,
  EventCounter,
  ExchangeGetContractResponse,
  ExchangeKeysResponse,
  ExchangeKycUploadFormRequest,
  ExchangeMeltRequestV2,
  ExchangeMeltResponse,
  ExchangeMergeConflictResponse,
  ExchangeMergeSuccessResponse,
  ExchangePurseDeposits,
  ExchangePurseMergeRequest,
  ExchangePurseStatus,
  ExchangeRefreshRevealRequestV2,
  ExchangeReservePurseRequest,
  ExchangeTransferList,
  ExchangeVersionResponse,
  ExchangeWithdrawRequest,
  ExchangeWithdrawResponse,
  KycAttributes,
  KycProcessClientInformation,
  KycProcessClientInformationWithEtag,
  KycProcessStartInformation,
  KycRequirementInformationId,
  LegitimizationMeasuresList,
  LegitimizationNeededResponse,
  PurseConflict,
  PurseConflictPartial,
  WalletKycRequest,
  codecForAccountKycStatus,
  codecForAmlDecisionsAccounts,
  codecForAmlDecisionsResponse,
  codecForAmlKycAttributes,
  codecForAmlStatisticsResponse,
  codecForAmlWalletKycCheckResponse,
  codecForAvailableMeasureSummary,
  codecForExchangeConfig,
  codecForExchangeGetContractResponse,
  codecForExchangeKeysResponse,
  codecForExchangeMeltResponse,
  codecForExchangeMergeConflictResponse,
  codecForExchangeMergeSuccessResponse,
  codecForExchangePurseStatus,
  codecForExchangeTransferList,
  codecForExchangeWithdrawResponse,
  codecForKycProcessClientInformation,
  codecForKycProcessStartInformation,
  codecForLegitimizationMeasuresList,
  codecForLegitimizationNeededResponse,
  codecForPurseConflict,
  codecForPurseConflictPartial,
} from "../types-taler-exchange.js";
import {
  CacheEvictor,
  addLongPollingParam,
  addPaginationParams,
  nullEvictor,
} from "./utils.js";

import {
  AmountJson,
  Amounts,
  CancellationToken,
  LongpollQueue,
  PaytoHash,
  signAmlDecision,
  signAmlQuery,
} from "../index.js";
import { AbsoluteTime } from "../time.js";
import { EmptyObject, codecForEmptyObject } from "../types-taler-wallet.js";

export type TalerExchangeResultByMethod2<
  prop extends keyof TalerExchangeHttpClient,
> = ResultByMethod<TalerExchangeHttpClient, prop>;
export type TalerExchangeErrorsByMethod2<
  prop extends keyof TalerExchangeHttpClient,
> = FailCasesByMethod<TalerExchangeHttpClient, prop>;

export enum TalerExchangeCacheEviction {
  UPLOAD_KYC_FORM,
  MAKE_AML_DECISION,
}

/**
 * Client library for the GNU Taler exchange service.
 */
export class TalerExchangeHttpClient {
  public static readonly SUPPORTED_EXCHANGE_PROTOCOL_VERSION = "31:0:6";
  private httpLib: HttpRequestLibrary;
  private cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>;
  private preventCompression: boolean;
  private cancelationToken: CancellationToken;
  private longPollQueue: LongpollQueue;

  constructor(
    readonly baseUrl: string,
    params: {
      httpClient?: HttpRequestLibrary;
      cacheEvictor?: CacheEvictor<TalerExchangeCacheEviction>;
      preventCompression?: boolean;
      cancelationToken?: CancellationToken;
      longPollQueue?: LongpollQueue;
    } = {},
  ) {
    this.httpLib = params.httpClient ?? createPlatformHttpLib();
    this.cacheEvictor = params.cacheEvictor ?? nullEvictor;
    this.preventCompression = !!params.preventCompression;
    this.cancelationToken =
      params.cancelationToken ?? CancellationToken.CONTINUE;
    this.longPollQueue = params.longPollQueue ?? new LongpollQueue();
  }

  static isCompatible(version: string): boolean {
    const compare = LibtoolVersion.compare(
      TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION,
      version,
    );
    return compare?.compatible ?? false;
  }

  private async fetch(
    url_or_path: URL | string,
    opts: HttpRequestOptions = {},
    longpoll: boolean = false,
  ): Promise<HttpResponse> {
    const url =
      typeof url_or_path == "string"
        ? new URL(url_or_path, this.baseUrl)
        : url_or_path;
    if (longpoll || url.searchParams.has("timeout_ms")) {
      return this.longPollQueue.run(
        url,
        this.cancelationToken,
        async (timeoutMs) => {
          url.searchParams.set("timeout_ms", String(timeoutMs));
          return this.httpLib.fetch(url.href, {
            cancellationToken: this.cancelationToken,
            ...opts,
          });
        },
      );
    } else {
      return this.httpLib.fetch(url.href, {
        cancellationToken: this.cancelationToken,
        ...opts,
      });
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--seed
   *
   */
  async getSeed() {
    const resp = await this.fetch("seed");
    switch (resp.status) {
      case HttpStatusCode.Ok:
        const buffer = await resp.bytes();
        const uintar = new Uint8Array(buffer);
        return opFixedSuccess(uintar);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }
  /**
   * https://docs.taler.net/core/api-exchange.html#get--config
   *
   */
  async getConfig(): Promise<
    | OperationFail<HttpStatusCode.NotFound>
    | OperationOk<ExchangeVersionResponse>
  > {
    const resp = await this.fetch("config");
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return carefullyParseConfig(
          "taler-exchange",
          TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION,
          resp,
          codecForExchangeConfig(),
        );
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get--config
   *
   * PARTIALLY IMPLEMENTED!!
   */
  async getKeys(): Promise<OperationOk<ExchangeKeysResponse>> {
    const resp = await this.fetch("keys");
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeKeysResponse());
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  // WALLET TO WALLET

  /**
   * https://docs.taler.net/core/api-exchange.html#get--purses-$PURSE_PUB-merge
   *
   */
  async getPurseStatusAtMerge(
    pursePub: string,
    longpoll: boolean = false,
  ): Promise<
    | OperationOk<ExchangePurseStatus>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationFail<HttpStatusCode.Gone>
  > {
    const resp = await this.fetch(`purses/${pursePub}/merge`, {}, longpoll);
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangePurseStatus());
      case HttpStatusCode.Gone:
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--purses-$PURSE_PUB-deposit
   *
   */
  async getPurseStatusAtDeposit(
    pursePub: string,
    longpoll: boolean = false,
  ): Promise<
    | OperationOk<ExchangePurseStatus>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationFail<HttpStatusCode.Gone>
  > {
    const resp = await this.fetch(`purses/${pursePub}/deposit`, {}, longpoll);
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangePurseStatus());
      case HttpStatusCode.Gone:
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-create
   *
   */
  async createPurseFromDeposit(
    pursePub: string,
    body: any, // FIXME
  ): Promise<
    | OperationOk<void>
    | OperationFail<HttpStatusCode.Forbidden>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationAlternative<HttpStatusCode.Conflict, PurseConflict>
    | OperationFail<HttpStatusCode.TooEarly>
  > {
    const resp = await this.fetch(`purses/${pursePub}/create`, {
      method: "POST",
      body,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        // FIXME: parse PurseCreateSuccessResponse
        return opSuccessFromHttp(resp, codecForAny());
      case HttpStatusCode.Conflict:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForPurseConflict(),
        );
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.TooEarly:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#delete--purses-$PURSE_PUB
   *
   */
  async deletePurse(
    pursePub: string,
    purseSig: string,
  ): Promise<
    | OperationOk<void>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationFail<HttpStatusCode.Conflict>
    | OperationFail<HttpStatusCode.Forbidden>
  > {
    const resp = await this.fetch(`purses/${pursePub}`, {
      method: "DELETE",
      headers: {
        "taler-purse-signature": purseSig,
      },
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent:
        return opEmptySuccess();
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * POST /purses/$PURSE_PUB/merge
   *
   * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
   */
  async postPurseMerge(
    pursePub: string,
    body: ExchangePurseMergeRequest,
  ): Promise<
    | OperationOk<ExchangeMergeSuccessResponse>
    | OperationAlternative<
        HttpStatusCode.UnavailableForLegalReasons,
        LegitimizationNeededResponse
      >
    | OperationAlternative<
        HttpStatusCode.Conflict,
        ExchangeMergeConflictResponse
      >
    | OperationFail<HttpStatusCode.Forbidden>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationFail<HttpStatusCode.Gone>
  > {
    const resp = await this.fetch(`purses/${pursePub}/merge`, {
      method: "POST",
      body,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeMergeSuccessResponse());
      case HttpStatusCode.UnavailableForLegalReasons:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForLegitimizationNeededResponse(),
        );
      case HttpStatusCode.Conflict:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForExchangeMergeConflictResponse(),
        );
      case HttpStatusCode.Gone:
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-purse
   *
   */
  async createPurseFromReserve(
    pursePub: string,
    body: ExchangeReservePurseRequest,
  ): Promise<
    | OperationOk<void>
    | OperationFail<HttpStatusCode.PaymentRequired>
    | OperationFail<HttpStatusCode.Forbidden>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationAlternative<HttpStatusCode.Conflict, PurseConflictPartial>
    | OperationAlternative<
        HttpStatusCode.UnavailableForLegalReasons,
        LegitimizationNeededResponse
      >
  > {
    const resp = await this.fetch(`reserves/${pursePub}/purse`, {
      method: "POST",
      body,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        // FIXME: parse PurseCreateSuccessResponse
        return opSuccessFromHttp(resp, codecForAny());
      case HttpStatusCode.PaymentRequired:
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForPurseConflictPartial(),
        );
      case HttpStatusCode.UnavailableForLegalReasons:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForLegitimizationNeededResponse(),
        );
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--contracts-$CONTRACT_PUB
   *
   */
  async getContract(
    pursePub: string,
  ): Promise<
    | OperationOk<ExchangeGetContractResponse>
    | OperationFail<HttpStatusCode.NotFound>
  > {
    const resp = await this.fetch(`contracts/${pursePub}`);
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeGetContractResponse());
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-deposit
   *
   */
  async depositIntoPurse(
    pursePub: string,
    body: ExchangePurseDeposits,
  ): Promise<
    | OperationOk<void>
    | OperationAlternative<HttpStatusCode.Conflict, PurseConflict>
    | OperationFail<HttpStatusCode.Forbidden>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationFail<HttpStatusCode.Gone>
  > {
    const resp = await this.fetch(`purses/${pursePub}/deposit`, {
      method: "POST",
      body,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        // FIXME: parse PurseDepositSuccessResponse
        return opSuccessFromHttp(resp, codecForAny());
      case HttpStatusCode.Conflict:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForPurseConflict(),
        );
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Gone:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  // WADS

  /**
   * https://docs.taler.net/core/api-exchange.html#get--wads-$WAD_ID
   *
   */
  async getWadInfo(): Promise<never> {
    throw Error("not yet implemented");
  }

  //
  // KYC
  //

  /**
   * https://docs.taler.net/core/api-exchange.html#post--kyc-wallet
   *
   */
  async notifyKycBalanceLimit(body: WalletKycRequest) {
    const url = new URL(`kyc-wallet`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAmlWalletKycCheckResponse());
      case HttpStatusCode.NoContent:
        return opEmptySuccess();
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.UnavailableForLegalReasons:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForLegitimizationNeededResponse(),
        );
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--kyc-check-$H_NORMALIZED_PAYTO
   *
   */
  async checkKycStatus(args: {
    paytoHash: string;
    accountPub: EddsaPublicKeyString;
    accountSig: EddsaSignatureString;
    longpoll?: boolean;
    awaitAuth?: boolean;
  }) {
    const { paytoHash, accountPub, accountSig, longpoll, awaitAuth } = args;
    const url = new URL(`kyc-check/${paytoHash}`, this.baseUrl);
    if (awaitAuth !== undefined) {
      url.searchParams.set("await_auth", awaitAuth ? "YES" : "NO");
    }

    const resp = await this.fetch(
      url,
      {
        headers: {
          "Account-Owner-Signature": accountSig,
          "Account-Owner-Pub": accountPub,
        },
      },
      longpoll,
    );

    switch (resp.status) {
      case HttpStatusCode.Ok: // means there are voluntary checks
      case HttpStatusCode.Accepted: // means there are requried checks
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForAccountKycStatus(),
        );
      case HttpStatusCode.NoContent: // no checks can be done
        return opEmptySuccess();
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * Do a /kyc-check request, but don't specify
   * the account pub explicitly.
   *
   * Deprecated, but used in tests.
   */
  async testingCheckKycStatusNoPub(args: {
    paytoHash: string;
    accountSig: EddsaSignatureString;
    longpoll?: boolean;
    awaitAuth?: boolean;
  }): Promise<
    | OperationOk<void>
    | OperationAlternative<HttpStatusCode.Ok, AccountKycStatus>
    | OperationAlternative<HttpStatusCode.Accepted, AccountKycStatus>
    | OperationFail<HttpStatusCode.Forbidden>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationFail<HttpStatusCode.Conflict>
  > {
    const { paytoHash, accountSig, longpoll, awaitAuth } = args;
    const url = new URL(`kyc-check/${paytoHash}`, this.baseUrl);
    if (awaitAuth !== undefined) {
      url.searchParams.set("await_auth", awaitAuth ? "YES" : "NO");
    }

    const resp = await this.fetch(
      url,
      {
        headers: {
          "Account-Owner-Signature": accountSig,
        },
      },
      longpoll,
    );

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

  /**
   * https://docs.taler.net/core/api-exchange.html#get--kyc-info-$ACCESS_TOKEN
   *
   */
  async checkKycInfo(
    token: AccessToken,
    known: KycRequirementInformationId[] = [],
    longpoll: boolean = false,
  ): Promise<
    | OperationOk<KycProcessClientInformation>
    | OperationAlternative<HttpStatusCode.Accepted, EmptyObject>
    | OperationAlternative<HttpStatusCode.NoContent, EmptyObject>
    | OperationFail<HttpStatusCode.NotModified>
  > {
    const resp = await this.fetch(
      `kyc-info/${token}`,
      {
        method: "GET",
        headers: {
          "If-None-Match": known.length ? known.join(",") : undefined,
        },
      },
      longpoll,
    );
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForKycProcessClientInformation());
      case HttpStatusCode.Accepted:
      case HttpStatusCode.NoContent:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForEmptyObject(),
        );
      case HttpStatusCode.NotModified:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * SPA-Specific version of checkKycInfo
   */
  async checkKycInfoSpa(
    token: AccessToken,
    etag: string | undefined,
    params: LongPollParams = {},
  ) {
    const url = new URL(`kyc-info/${token}`, this.baseUrl);

    addLongPollingParam(url, params);

    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers: {
        "If-None-Match": etag,
      },
    });
    switch (resp.status) {
      case HttpStatusCode.Ok: {
        // we need to add the etag to the response because the
        // client needs it to repeat the request and
        // do the long polling
        const etag = resp.headers.get("etag") ?? undefined;
        const body = await readSuccessResponseJsonOrThrow(
          resp,
          codecForKycProcessClientInformation(),
        );
        return opFixedSuccess<KycProcessClientInformationWithEtag>({
          ...body,
          etag,
        });
      }
      case HttpStatusCode.Accepted:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NoContent:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotModified:
        // do not read details from response
        return opKnownFailure(resp.status);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--kyc-upload-$ID
   *
   */
  async uploadKycForm<T extends ExchangeKycUploadFormRequest>(
    requirement: KycRequirementInformationId,
    body: T,
  ): Promise<
    | OperationOk<void>
    | OperationFail<HttpStatusCode.Conflict>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationFail<HttpStatusCode.PayloadTooLarge>
  > {
    const resp = await this.fetch(`kyc-upload/${requirement}`, {
      method: "POST",
      body,
      compress: this.preventCompression ? undefined : "deflate",
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerExchangeCacheEviction.UPLOAD_KYC_FORM,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
      case HttpStatusCode.PayloadTooLarge:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--kyc-start-$ID
   *
   */
  async startExternalKycProcess(
    requirement: KycRequirementInformationId,
    body: object = {},
  ): Promise<
    | OperationFail<
        | HttpStatusCode.NotFound
        | HttpStatusCode.Conflict
        | HttpStatusCode.PayloadTooLarge
      >
    | OperationOk<KycProcessStartInformation>
  > {
    const resp = await this.fetch(`kyc-start/${requirement}`, {
      method: "POST",
      body,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForKycProcessStartInformation());
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
      case HttpStatusCode.PayloadTooLarge:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--kyc-proof-$PROVIDER_NAME?state=$H_PAYTO
   *
   */
  async completeExternalKycProcess(
    provider: string,
    state: string,
    code: string,
  ) {
    const resp = await this.fetch(
      `kyc-proof/${provider}?state=${state}&code=${code}`,
      {
        method: "GET",
        redirect: "manual",
      },
    );

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

  //
  // AML operations
  //

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures
   *
   */
  async getAmlMeasures(
    auth: OfficerAccount,
  ): Promise<
    | OperationOk<AvailableMeasureSummary>
    | OperationFail<
        | HttpStatusCode.Forbidden
        | HttpStatusCode.NotFound
        | HttpStatusCode.Conflict
      >
  > {
    const resp = await this.fetch(`aml/${auth.id}/measures`, {
      method: "GET",
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(auth.signingKey),
        ),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAvailableMeasureSummary());
      case HttpStatusCode.Conflict:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-kyc-statistics-$NAMES
   *
   */
  async getAmlKycStatistics(
    auth: OfficerAccount,
    names: string[],
    filter: {
      since?: AbsoluteTime;
      until?: AbsoluteTime;
    } = {},
  ) {
    const url = new URL(
      `aml/${auth.id}/kyc-statistics/${names.join(" ")}`,
      this.baseUrl,
    );

    if (filter.since !== undefined && filter.since.t_ms !== "never") {
      url.searchParams.set("start_date", String(filter.since.t_ms));
    }
    if (filter.until !== undefined && filter.until.t_ms !== "never") {
      url.searchParams.set("end_date", String(filter.until.t_ms));
    }

    const resp = await this.fetch(url, {
      method: "GET",
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(auth.signingKey),
        ),
      },
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAmlStatisticsResponse());
      case HttpStatusCode.NoContent: {
        return opFixedSuccess({
          statistics: names.map(
            (name) => ({ counter: 0, name }) as EventCounter,
          ),
        });
      }
      case HttpStatusCode.Conflict:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions
   *
   */
  async getAmlAccounts(
    auth: OfficerAccount,
    params: PaginationParams & {
      highRisk?: boolean;
      open?: boolean;
      investigation?: boolean;
    } = {},
  ) {
    const url = new URL(`aml/${auth.id}/accounts`, this.baseUrl);

    addPaginationParams(url, params);
    if (params.investigation !== undefined) {
      url.searchParams.set(
        "investigation",
        params.investigation ? "YES" : "NO",
      );
    }
    if (params.open !== undefined && params.open) {
      url.searchParams.set("open", "YES");
    }
    if (params.highRisk !== undefined && params.highRisk) {
      url.searchParams.set("high_risk", "YES");
    }

    const resp = await this.fetch(url, {
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(auth.signingKey),
        ),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAmlDecisionsAccounts());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({ accounts: [] });
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions
   *
   */
  async getAmlDecisions(
    auth: OfficerAccount,
    params: PaginationParams & {
      account?: PaytoHash;
      active?: boolean;
      investigation?: boolean;
    } = {},
  ): Promise<
    | OperationFail<
        | HttpStatusCode.Forbidden
        | HttpStatusCode.NotFound
        | HttpStatusCode.Conflict
      >
    | OperationOk<AmlDecisionsResponse>
  > {
    const url = new URL(`aml/${auth.id}/decisions`, this.baseUrl);

    addPaginationParams(url, params);
    if (params.account !== undefined) {
      url.searchParams.set("h_payto", params.account);
    }
    if (params.active !== undefined) {
      url.searchParams.set("active", params.active ? "YES" : "NO");
    }
    if (params.investigation !== undefined) {
      url.searchParams.set(
        "investigation",
        params.investigation ? "YES" : "NO",
      );
    }

    const resp = await this.fetch(url, {
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(auth.signingKey),
        ),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAmlDecisionsResponse());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({ records: [] });
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-legitimizations
   */
  async getAmlLegitimizations(
    officer: OfficerAccount,
    params: PaginationParams & {
      account?: PaytoHash;
      active?: boolean;
    } = {},
  ): Promise<OperationOk<LegitimizationMeasuresList>> {
    const url = new URL(`aml/${officer.id}/legitimizations`, this.baseUrl);

    addPaginationParams(url, params);
    if (params.account !== undefined) {
      url.searchParams.set("h_payto", params.account);
    }
    if (params.active !== undefined) {
      url.searchParams.set("active", params.active ? "YES" : "NO");
    }

    const resp = await this.httpLib.fetch(url.href, {
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(officer.signingKey),
        ),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForLegitimizationMeasuresList());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({
          measures: [],
        });
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO
   *
   */
  async getAmlAttributesForAccount(
    auth: OfficerAccount,
    account: string,
    params: PaginationParams = {},
  ): Promise<
    | OperationOk<KycAttributes>
    | OperationFail<
        | HttpStatusCode.Forbidden
        | HttpStatusCode.NotFound
        | HttpStatusCode.Conflict
      >
  > {
    const url = new URL(`aml/${auth.id}/attributes/${account}`, this.baseUrl);

    addPaginationParams(url, params);
    const resp = await this.fetch(url, {
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(auth.signingKey),
        ),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAmlKycAttributes());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({ details: [] });
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision
   *
   */
  async makeAmlDesicion(
    auth: OfficerAccount,
    decision: Omit<AmlDecisionRequest, "officer_sig">,
  ): Promise<
    | OperationOk<void>
    | OperationFail<
        | HttpStatusCode.Forbidden
        | HttpStatusCode.NotFound
        | HttpStatusCode.Conflict
      >
  > {
    const body: AmlDecisionRequest = {
      officer_sig: encodeCrock(
        signAmlDecision(auth.signingKey, decision),
      ) as any,
      ...decision,
    };
    const resp = await this.fetch(`aml/${auth.id}/decision`, {
      method: "POST",
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(auth.signingKey),
        ),
      },
      body,
      compress: this.preventCompression ? undefined : "deflate",
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerExchangeCacheEviction.MAKE_AML_DECISION,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-transfers-credit
   *
   */
  async getTransfersCredit(
    auth: OfficerAccount,
    params: PaginationParams & {
      threshold?: AmountJson;
      account?: PaytoHash;
    } = {},
  ): Promise<
    | OperationOk<ExchangeTransferList>
    | OperationFail<
        | HttpStatusCode.Forbidden
        | HttpStatusCode.NotFound
        | HttpStatusCode.Conflict
      >
  > {
    const url = new URL(`aml/${auth.id}/transfers-credit`, this.baseUrl);

    addPaginationParams(url, params);

    if (params.threshold) {
      url.searchParams.set("threshold", Amounts.stringify(params.threshold));
    }
    if (params.account) {
      url.searchParams.set("h_payto", params.account);
    }

    const resp = await this.fetch(url, {
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(auth.signingKey),
        ),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeTransferList());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({ transfers: [] });
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-transfers-debit
   *
   */
  async getTransfersDebit(
    auth: OfficerAccount,
    params: PaginationParams & {
      threshold?: AmountJson;
      account?: PaytoHash;
    } = {},
  ): Promise<
    | OperationOk<ExchangeTransferList>
    | OperationFail<
        | HttpStatusCode.Forbidden
        | HttpStatusCode.NotFound
        | HttpStatusCode.Conflict
      >
  > {
    const url = new URL(`aml/${auth.id}/transfers-debit`, this.baseUrl);

    addPaginationParams(url, params);

    if (params.threshold) {
      url.searchParams.set("threshold", Amounts.stringify(params.threshold));
    }
    if (params.account) {
      url.searchParams.set("h_payto", params.account);
    }

    const resp = await this.fetch(url, {
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(auth.signingKey),
        ),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeTransferList());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({ transfers: [] });
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-transfers-kycauth
   *
   */
  async getTransfersKycAuth(
    auth: OfficerAccount,
    params: PaginationParams & {
      threshold?: AmountJson;
      account?: PaytoHash;
    } = {},
  ): Promise<
    | OperationOk<ExchangeTransferList>
    | OperationFail<
        | HttpStatusCode.Forbidden
        | HttpStatusCode.NotFound
        | HttpStatusCode.Conflict
      >
  > {
    const url = new URL(`aml/${auth.id}/transfers-kycauth`, this.baseUrl);

    addPaginationParams(url, params);

    if (params.threshold) {
      url.searchParams.set("threshold", Amounts.stringify(params.threshold));
    }
    if (params.account) {
      url.searchParams.set("h_payto", params.account);
    }

    const resp = await this.fetch(url, {
      headers: {
        "Taler-AML-Officer-Signature": encodeCrock(
          signAmlQuery(auth.signingKey),
        ),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeTransferList());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({ transfers: [] });
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * Request: POST /withdraw
   *
   * https://docs.taler.net/core/api-exchange.html#withdrawal
   */
  async withdraw(args: {
    body: ExchangeWithdrawRequest;
  }): Promise<
    | OperationOk<ExchangeWithdrawResponse>
    | OperationFail<HttpStatusCode.Forbidden>
  > {
    const url = new URL(`withdraw`, this.baseUrl);
    const resp = await this.fetch(url, {
      method: "POST",
      body: args.body,
    });
    // FIXME: Some documented cases are missing.
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeWithdrawResponse());
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * Request: POST /melt
   *
   * https://docs.taler.net/core/api-exchange.html#post--melt
   */
  async postMelt(args: {
    body: ExchangeMeltRequestV2;
  }): Promise<
    OperationOk<ExchangeMeltResponse> | OperationFail<HttpStatusCode.Forbidden>
  > {
    const url = new URL(`melt`, this.baseUrl);
    const resp = await this.fetch(url, {
      method: "POST",
      body: args.body,
    });
    // FIXME: Some documented cases are missing.
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeMeltResponse());
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  async postRevealMelt(args: {
    body: ExchangeRefreshRevealRequestV2;
  }): Promise<
    | OperationOk<ExchangeWithdrawResponse>
    | OperationFail<HttpStatusCode.Forbidden>
  > {
    const url = new URL(`reveal-melt`, this.baseUrl);
    const resp = await this.fetch(url, {
      method: "POST",
      body: args.body,
    });
    // FIXME: Some documented cases are missing.
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeWithdrawResponse());
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }
}
