import {
  HttpRequestLibrary,
  readSuccessResponseJsonOrThrow,
  readTalerErrorResponse,
} from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import { LibtoolVersion } from "../libtool-version.js";
import { hash } from "../nacl-fast.js";
import {
  FailCasesByMethod,
  OperationFail,
  OperationOk,
  ResultByMethod,
  opEmptySuccess,
  opFixedSuccess,
  opKnownAlternativeFailure,
  opKnownHttpFailure,
  opSuccessFromHttp,
  opUnknownFailure,
} from "../operation.js";
import { Codec, codecForAny } from "../codec.js";
import {
  TalerSignaturePurpose,
  bufferForUint64,
  buildSigPS,
  decodeCrock,
  eddsaSign,
  encodeCrock,
  stringToBytes,
  timestampRoundedToBuffer,
} from "../taler-crypto.js";
import {
  AccessToken,
  AmountString,
  OfficerAccount,
  PaginationParams,
  ReserveAccount,
  SigningKey,
  codecForTalerCommonConfigResponse,
} from "../types-taler-common.js";
import {
  AmlDecisionRequest,
  BatchWithdrawResponse,
  ExchangeBatchWithdrawRequest,
  ExchangeVersionResponse,
  KycRequirementInformationId,
  WalletKycRequest,
  codecForAccountKycStatus,
  codecForAmlDecisionsResponse,
  codecForAmlKycAttributes,
  codecForAmlWalletKycCheckResponse,
  codecForAvailableMeasureSummary,
  codecForEventCounter,
  codecForExchangeConfig,
  codecForExchangeKeys,
  codecForKycProcessClientInformation,
  codecForKycProcessStartInformation,
  codecForLegitimizationNeededResponse,
} from "../types-taler-exchange.js";
import {
  CacheEvictor,
  addMerchantPaginationParams,
  nullEvictor,
} from "./utils.js";

import { TalerError } from "../errors.js";
import { TalerErrorCode } from "../taler-error-codes.js";
import { codecForEmptyObject } from "../types-taler-wallet.js";
import { canonicalJson } from "../helpers.js";

export type TalerExchangeResultByMethod<
  prop extends keyof TalerExchangeHttpClient,
> = ResultByMethod<TalerExchangeHttpClient, prop>;
export type TalerExchangeErrorsByMethod<
  prop extends keyof TalerExchangeHttpClient,
> = FailCasesByMethod<TalerExchangeHttpClient, prop>;

export enum TalerExchangeCacheEviction {
  CREATE_DESCISION,
  UPLOAD_KYC_FORM,
  MAKE_AML_DECISION,
}

declare const __pubId: unique symbol;
export type ReservePub = string & { [__pubId]: true };
/**
 */
export class TalerExchangeHttpClient {
  httpLib: HttpRequestLibrary;
  public readonly PROTOCOL_VERSION = "20:0:0";
  cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>;

  constructor(
    readonly baseUrl: string,
    httpClient?: HttpRequestLibrary,
    cacheEvictor?: CacheEvictor<TalerExchangeCacheEviction>,
  ) {
    this.httpLib = httpClient ?? createPlatformHttpLib();
    this.cacheEvictor = cacheEvictor ?? nullEvictor;
  }

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

  // TERMS

  /**
   * https://docs.taler.net/core/api-exchange.html#get--seed
   *
   */
  /**
   * https://docs.taler.net/core/api-exchange.html#get--seed
   *
   */

  // EXCHANGE INFORMATION

  /**
   * https://docs.taler.net/core/api-exchange.html#get--seed
   *
   */
  async getSeed() {
    const url = new URL(`seed`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
    });
    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 opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }
  /**
   * https://docs.taler.net/core/api-exchange.html#get--config
   *
   */
  async getConfig(): Promise<
    | OperationFail<HttpStatusCode.NotFound>
    | OperationOk<ExchangeVersionResponse>
  > {
    const url = new URL(`config`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
    });
    switch (resp.status) {
      case HttpStatusCode.Ok: {
        const minBody = await readSuccessResponseJsonOrThrow(
          resp,
          codecForTalerCommonConfigResponse(),
        );
        const expectedName = "taler-exchange";
        if (minBody.name !== expectedName) {
          throw TalerError.fromUncheckedDetail({
            code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
            requestUrl: resp.requestUrl,
            httpStatusCode: resp.status,
            detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`,
          });
        }
        if (!this.isCompatible(minBody.version)) {
          throw TalerError.fromUncheckedDetail({
            code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION,
            requestUrl: resp.requestUrl,
            httpStatusCode: resp.status,
            detail: `Unsupported protocol version, client supports ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`,
          });
        }
        // Now that we've checked the basic body, re-parse the full response.
        const body = await readSuccessResponseJsonOrThrow(
          resp,
          codecForExchangeConfig(),
        );
        return {
          type: "ok",
          body,
        };
      }
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get--config
   *
   * PARTIALLY IMPLEMENTED!!
   */
  async getKeys() {
    const url = new URL(`keys`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForExchangeKeys());
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  //
  // MANAGEMENT
  //

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

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-keys
   *
   */
  async signFutureKeys(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-denominations-$H_DENOM_PUB-revoke
   *
   */
  async revokeFutureDenominationKeys(): Promise<never> {
    throw Error("not yet implemented");
  }
  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-signkeys-$EXCHANGE_PUB-revoke
   *
   */
  async revokeFutureSigningKeys(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-auditors
   *
   */
  async enableAuditor(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-auditors-$AUDITOR_PUB-disable
   *
   */
  async disableAuditor(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-wire-fee
   *
   */
  async configWireFee(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-global-fees
   *
   */
  async configGlobalFees(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-wire
   *
   */
  async enableWireMethod(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-wire-disable
   *
   */
  async disableWireMethod(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-drain
   *
   */
  async drainProfits(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-aml-officers
   *
   */
  async updateOfficer(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--management-partners
   *
   */
  async enablePartner(): Promise<never> {
    throw Error("not yet implemented");
  }

  //
  // AUDITOR
  //

  /**
   * https://docs.taler.net/core/api-exchange.html#post--auditors-$AUDITOR_PUB-$H_DENOM_PUB
   *
   */
  async addAuditor(): Promise<never> {
    throw Error("not yet implemented");
  }

  //
  // WITHDRAWAL
  //

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

  /**
   * https://docs.taler.net/core/api-exchange.html#post--csr-withdraw
   *
   */
  async prepareCsrWithdawal(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-batch-withdraw
   *
   */
  async withdraw(rid: ReservePub, body: ExchangeBatchWithdrawRequest) {
    const url = new URL(`reserves/${rid}/batch-withdraw`, this.baseUrl);

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

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(
          resp,
          codecForAny() as Codec<BatchWithdrawResponse>,
        );
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.BadRequest:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Gone:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.UnavailableForLegalReasons:
        return opKnownAlternativeFailure(
          resp,
          resp.status,
          codecForLegitimizationNeededResponse(),
        );
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#withdraw-with-age-restriction
   *
   */
  async withdrawWithAge(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--age-withdraw-$ACH-reveal
   *
   */
  async revealCoinsForAge(): Promise<never> {
    throw Error("not yet implemented");
  }

  //
  // RESERVE HISTORY
  //

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

  //
  // COIN HISTORY
  //

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

  //
  // DEPOSIT
  //

  /**
   * https://docs.taler.net/core/api-exchange.html#post--batch-deposit
   *
   */
  async deposit(): Promise<never> {
    throw Error("not yet implemented");
  }

  //
  // REFRESH
  //

  /**
   * https://docs.taler.net/core/api-exchange.html#post--csr-melt
   *
   */
  async prepareCsrMelt(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-melt
   *
   */
  async meltCoin(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--refreshes-$RCH-reveal
   *
   */
  async releaveCoin(): Promise<never> {
    throw Error("not yet implemented");
  }

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

  //
  // RECOUP
  //

  /**
   * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup
   *
   */
  async recoupReserveCoin(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup-refresh
   *
   */
  async recoupRefreshCoin(): Promise<never> {
    throw Error("not yet implemented");
  }

  // WIRE TRANSFER

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

  /**
   * https://docs.taler.net/core/api-exchange.html#get--deposits-$H_WIRE-$MERCHANT_PUB-$H_CONTRACT_TERMS-$COIN_PUB
   *
   */
  async getWireTransferIdForDeposit(): Promise<never> {
    throw Error("not yet implemented");
  }

  // REFUND

  /**
   * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-refund
   *
   */
  async refund(): Promise<never> {
    throw Error("not yet implemented");
  }

  // WALLET TO WALLET

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

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

  /**
   * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-create
   *
   */
  async createPurseFromDeposit(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#delete--purses-$PURSE_PUB
   *
   */
  async deletePurse(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
   *
   */
  async mergePurse(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-purse
   *
   */
  async createPurseFromReserve(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-deposit
   *
   */
  async depositIntoPurse(): Promise<never> {
    throw Error("not yet implemented");
  }

  // 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(account: ReserveAccount, balance: AmountString) {
    const url = new URL(`kyc-wallet`, this.baseUrl);

    const body: WalletKycRequest = {
      balance,
      reserve_pub: account.id,
      reserve_sig: encodeCrock(account.signingKey),
    };

    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(resp);
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.UnavailableForLegalReasons:
        return opKnownAlternativeFailure(
          resp,
          resp.status,
          codecForLegitimizationNeededResponse(),
        );
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--kyc-wallet
   *
   */
  async checkKycStatus(
    account: ReserveAccount,
    requirementId: number,
    params: {
      timeout?: number;
    } = {},
  ) {
    const url = new URL(`kyc-check/${String(requirementId)}`, this.baseUrl);

    if (params.timeout !== undefined) {
      url.searchParams.set("timeout_ms", String(params.timeout));
    }

    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers: {
        "Account-Owner-Signature": buildKYCQuerySignature(account.signingKey),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAccountKycStatus());
      case HttpStatusCode.Accepted:
        return opKnownAlternativeFailure(
          resp,
          resp.status,
          codecForAccountKycStatus(),
        );
      case HttpStatusCode.NoContent:
        return opEmptySuccess(resp);
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--kyc-info-$ACCESS_TOKEN
   *
   */
  async checkKycInfo(
    token: AccessToken,
    known: KycRequirementInformationId[],
    params: {
      timeout?: number;
    } = {},
  ) {
    const url = new URL(`kyc-info/${token}`, this.baseUrl);

    if (params.timeout !== undefined) {
      url.searchParams.set("timeout_ms", String(params.timeout));
    }

    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers: {
        "If-None-Match": known.length ? known.join(",") : undefined,
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForKycProcessClientInformation());
      case HttpStatusCode.NoContent:
        return opKnownAlternativeFailure(
          resp,
          HttpStatusCode.NoContent,
          codecForEmptyObject(),
        );
      case HttpStatusCode.NotModified:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--kyc-upload-$ID
   *
   */
  async uploadKycForm(requirement: KycRequirementInformationId, body: object) {
    const url = new URL(`kyc-upload/${requirement}`, this.baseUrl);

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

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerExchangeCacheEviction.UPLOAD_KYC_FORM,
        );
        return opEmptySuccess(resp);
      }
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.PayloadTooLarge:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--kyc-start-$ID
   *
   */
  async startExternalKycProcess(
    requirement: KycRequirementInformationId,
    body: object = {},
  ) {
    const url = new URL(`kyc-start/${requirement}`, this.baseUrl);

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

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForKycProcessStartInformation());
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.PayloadTooLarge:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#get--kyc-proof-$PROVIDER_NAME?state=$H_PAYTO
   *
   */
  async completeExternalKycProcess(provider: string, state: string) {}

  //
  // AML operations
  //

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures
   *
   */
  async getAmlMesasures(auth: OfficerAccount) {
    const url = new URL(`aml/${auth.id}/measures`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers: {
        "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
      },
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAvailableMeasureSummary());
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

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

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

    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers: {
        "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
      },
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForEventCounter());
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

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

    addMerchantPaginationParams(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.httpLib.fetch(url.href, {
      method: "GET",
      headers: {
        "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
      },
    });

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

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO
   *
   */
  async getAmlAttributesForAccount(
    auth: OfficerAccount,
    account: string,
    params: PaginationParams = {},
  ) {
    const url = new URL(`aml/${auth.id}/attributes/${account}`, this.baseUrl);

    addMerchantPaginationParams(url, params);
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers: {
        "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
      },
    });

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

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO
   *
   */
  async makeAmlDesicion(
    auth: OfficerAccount,
    decision: Omit<AmlDecisionRequest, "officer_sig">,
  ) {
    const url = new URL(`aml/${auth.id}/decision`, this.baseUrl);

    const body = buildAMLDecisionSignature(auth.signingKey, decision);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      headers: {
        "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
      },
      body,
    });

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

  // RESERVE control

  /**
   * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-open
   *
   */
  async reserveOpen(): Promise<never> {
    throw Error("not yet implemented");
  }

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

  /**
   * https://docs.taler.net/core/api-exchange.html#post--reserves-attest-$RESERVE_PUB
   *
   */
  async signReserveAttributes(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-close
   *
   */
  async closeReserve(): Promise<never> {
    throw Error("not yet implemented");
  }

  /**
   * https://docs.taler.net/core/api-exchange.html#delete--reserves-$RESERVE_PUB
   *
   */
  async deleteReserve(): Promise<never> {
    throw Error("not yet implemented");
  }
}

function buildKYCQuerySignature(key: SigningKey): string {
  const sigBlob = buildSigPS(TalerSignaturePurpose.AML_QUERY).build();

  return encodeCrock(eddsaSign(sigBlob, key));
}

function buildAMLQuerySignature(key: SigningKey): string {
  const sigBlob = buildSigPS(TalerSignaturePurpose.AML_QUERY).build();

  return encodeCrock(eddsaSign(sigBlob, key));
}

function buildAMLDecisionSignature(
  key: SigningKey,
  decision: Omit<AmlDecisionRequest, "officer_sig">,
): AmlDecisionRequest {
  const zero = new Uint8Array(new ArrayBuffer(64));

  const sigBlob = buildSigPS(TalerSignaturePurpose.AML_DECISION)
    .put(timestampRoundedToBuffer(decision.decision_time))
    .put(decodeCrock(decision.h_payto))
    .put(hash(stringToBytes(decision.justification)))
    .put(hash(stringToBytes(canonicalJson(decision.properties) + "\0")))
    .put(hash(stringToBytes(canonicalJson(decision.new_rules) + "\0")))
    .put(
      decision.new_measure != null
        ? hash(stringToBytes(decision.new_measure))
        : zero,
    )
    .put(bufferForUint64(decision.keep_investigating ? 1 : 0))
    .build();

  const officer_sig = encodeCrock(eddsaSign(sigBlob, key));

  return {
    ...decision,
    officer_sig,
  };
}
