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,
  opKnownHttpFailure,
  opSuccessFromHttp,
  opUnknownFailure,
} from "../operation.js";
import {
  TalerSignaturePurpose,
  amountToBuffer,
  bufferForUint32,
  buildSigPS,
  decodeCrock,
  eddsaSign,
  encodeCrock,
  stringToBytes,
  timestampRoundedToBuffer,
} from "../taler-crypto.js";
import {
  OfficerAccount,
  PaginationParams,
  SigningKey,
  codecForTalerCommonConfigResponse,
} from "../types-taler-common.js";
import {
  codecForAmlDecisionDetails,
  codecForAmlRecords,
  codecForExchangeConfig,
  codecForExchangeKeys,
} from "../types-taler-exchange.js";
import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js";

import { TalerError } from "../errors.js";
import { TalerErrorCode } from "../taler-error-codes.js";
import * as TalerExchangeApi from "../types-taler-exchange.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,
}

/**
 */
export class TalerExchangeHttpClient {
  httpLib: HttpRequestLibrary;
  public readonly PROTOCOL_VERSION = "18:0:1";
  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;
  }
  /**
   * 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<TalerExchangeApi.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));
    }
  }

  // TERMS

  //
  // AML operations
  //

  /**
   * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions-$STATE
   *
   */
  async getDecisionsByState(
    auth: OfficerAccount,
    state: TalerExchangeApi.AmlState,
    pagination?: PaginationParams,
  ) {
    const url = new URL(
      `aml/${auth.id}/decisions/${TalerExchangeApi.AmlState[state]}`,
      this.baseUrl,
    );
    addPaginationParams(url, pagination);

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

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAmlRecords());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({ records: [] });
      //this should be unauthorized
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized:
        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-decision-$H_PAYTO
   *
   */
  async getDecisionDetails(auth: OfficerAccount, account: string) {
    const url = new URL(`aml/${auth.id}/decision/${account}`, this.baseUrl);

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

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAmlDecisionDetails());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({ aml_history: [], kyc_attributes: [] });
      //this should be unauthorized
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized:
        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#post--aml-$OFFICER_PUB-decision
   *
   */
  async addDecisionDetails(
    auth: OfficerAccount,
    decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">,
  ) {
    const url = new URL(`aml/${auth.id}/decision`, this.baseUrl);

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

    switch (resp.status) {
      case HttpStatusCode.NoContent:
        return opEmptySuccess(resp);
      //FIXME: this should be unauthorized
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      //FIXME: this two need to be split by error code
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }
}

function buildQuerySignature(key: SigningKey): string {
  const sigBlob = buildSigPS(
    TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY,
  ).build();

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

function buildDecisionSignature(
  key: SigningKey,
  decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">,
): TalerExchangeApi.AmlDecision {
  const zero = new Uint8Array(new ArrayBuffer(64));

  const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
    //TODO: new need the null terminator, also in the exchange
    .put(hash(stringToBytes(decision.justification))) //check null
    .put(timestampRoundedToBuffer(decision.decision_time))
    .put(amountToBuffer(decision.new_threshold))
    .put(decodeCrock(decision.h_payto))
    .put(zero) //kyc_requirement
    .put(bufferForUint32(decision.new_state))
    .build();

  const officer_sig = encodeCrock(eddsaSign(sigBlob, key));
  return {
    ...decision,
    officer_sig,
  };
}
