/*
 This file is part of GNU Taler
 (C) 2019-2020 Taler Systems SA
 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 
 */
/**
 * Classes and helpers for error handling specific to wallet operations.
 *
 * @author Florian Dold 
 */
/**
 * Imports.
 */
import {
  AbsoluteTime,
  PayMerchantInsufficientBalanceDetails,
  PayPeerInsufficientBalanceDetails,
  TalerErrorCode,
  TalerErrorDetail,
  TransactionType,
} from "@gnu-taler/taler-util";
type empty = Record;
export interface DetailsMap {
  [TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: {
    innerError: TalerErrorDetail;
    transactionId?: string;
  };
  [TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT]: {
    exchangeBaseUrl: string;
  };
  [TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE]: {
    exchangeProtocolVersion: string;
    walletProtocolVersion: string;
  };
  [TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK]: empty;
  [TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID]: empty;
  [TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: {
    orderId: string;
    claimUrl: string;
  };
  [TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: empty;
  [TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: {
    merchantPub: string;
    orderId: string;
  };
  [TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH]: {
    baseUrlForDownload: string;
    baseUrlFromContractTerms: string;
  };
  [TalerErrorCode.WALLET_INVALID_TALER_PAY_URI]: {
    talerPayUri: string;
  };
  [TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR]: {
    requestUrl: string;
    requestMethod: string;
    httpStatusCode: number;
    errorResponse?: any;
  };
  [TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: {
    stack?: string;
  };
  [TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {
    exchangeProtocolVersion: string;
    walletProtocolVersion: string;
  };
  [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {
    operation: string;
  };
  [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {
    requestUrl: string;
    requestMethod: string;
    throttleStats: Record;
  };
  [TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT]: empty;
  [TalerErrorCode.WALLET_NETWORK_ERROR]: {
    requestUrl: string;
    requestMethod: string;
  };
  [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {
    requestUrl: string;
    requestMethod: string;
    httpStatusCode: number;
    validationError?: string;
  };
  [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: empty;
  [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {
    errorsPerCoin: Record;
  };
  [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: empty;
  [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {
    httpStatusCode: number;
  };
  [TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR]: {
    requestError: TalerErrorDetail;
  };
  [TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR]: {
    innerError: TalerErrorDetail;
  };
  [TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST]: {
    detail: string;
  };
  [TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED]: {
    kycUrl: string;
  };
  [TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: {
    insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
  };
  [TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
    insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
  };
  [TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: {
    numErrors: number;
    /**
     * Errors, can be truncated.
     */
    errors: TalerErrorDetail[];
  };
}
type ErrBody = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
export function makeErrorDetail(
  code: C,
  detail: ErrBody,
  hint?: string,
): TalerErrorDetail {
  if (!hint && !(detail as any).hint) {
    hint = getDefaultHint(code);
  }
  const when = AbsoluteTime.now();
  return { code, when, hint, ...detail };
}
export function makePendingOperationFailedError(
  innerError: TalerErrorDetail,
  tag: TransactionType,
  uid: string,
): TalerError {
  return TalerError.fromDetail(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED, {
    innerError,
    transactionId: `${tag}:${uid}`,
  });
}
export function summarizeTalerErrorDetail(ed: TalerErrorDetail): string {
  const errName = TalerErrorCode[ed.code] ?? "";
  return `Error (${ed.code}/${errName})`;
}
function getDefaultHint(code: number): string {
  const errName = TalerErrorCode[code];
  if (errName) {
    return `Error (${errName})`;
  } else {
    return `Error ()`;
  }
}
export class TalerProtocolViolationError extends Error {
  constructor(hint?: string) {
    let msg: string;
    if (hint) {
      msg = `Taler protocol violation error (${hint})`;
    } else {
      msg = `Taler protocol violation error`;
    }
    super(msg);
    Object.setPrototypeOf(this, TalerProtocolViolationError.prototype);
  }
}
export class TalerError extends Error {
  errorDetail: TalerErrorDetail & T;
  private constructor(d: TalerErrorDetail & T) {
    super(d.hint ?? `Error (code ${d.code})`);
    this.errorDetail = d;
    Object.setPrototypeOf(this, TalerError.prototype);
  }
  static fromDetail(
    code: C,
    detail: ErrBody,
    hint?: string,
  ): TalerError {
    if (!hint) {
      hint = getDefaultHint(code);
    }
    const when = AbsoluteTime.now();
    return new TalerError({ code, when, hint, ...detail });
  }
  static fromUncheckedDetail(d: TalerErrorDetail): TalerError {
    return new TalerError({ ...d });
  }
  static fromException(e: any): TalerError {
    const errDetail = getErrorDetailFromException(e);
    return new TalerError(errDetail);
  }
  hasErrorCode(
    code: C,
  ): this is TalerError {
    return this.errorDetail.code === code;
  }
}
/**
 * Convert an exception (or anything that was thrown) into
 * a TalerErrorDetail object.
 */
export function getErrorDetailFromException(e: any): TalerErrorDetail {
  if (e instanceof TalerError) {
    return e.errorDetail;
  }
  if (e instanceof Error) {
    const err = makeErrorDetail(
      TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
      {
        stack: e.stack,
      },
      `unexpected exception (message: ${e.message})`,
    );
    return err;
  }
  // Something was thrown that is not even an exception!
  // Try to stringify it.
  let excString: string;
  try {
    excString = e.toString();
  } catch (e) {
    // Something went horribly wrong.
    excString = "can't stringify exception";
  }
  const err = makeErrorDetail(
    TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
    {},
    `unexpected exception (not an exception, ${excString})`,
  );
  return err;
}