/*
 This file is part of GNU Taler
 (C) 2019 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 
 */
import { InternalWalletState } from "./state";
import { parseTipUri } from "../util/taleruri";
import { TipStatus, OperationErrorDetails } from "../types/walletTypes";
import {
  TipPlanchetDetail,
  codecForTipPickupGetResponse,
  codecForTipResponse,
} from "../types/talerTypes";
import * as Amounts from "../util/amounts";
import {
  Stores,
  PlanchetRecord,
  WithdrawalGroupRecord,
  initRetryInfo,
  updateRetryInfoTimeout,
  WithdrawalSourceType,
  TipPlanchet,
} from "../types/dbTypes";
import {
  getExchangeWithdrawalInfo,
  selectWithdrawalDenoms,
  processWithdrawGroup,
  denomSelectionInfoToState,
} from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
import { guardOperationException } from "./errors";
import { NotificationType } from "../types/notifications";
import { getTimestampNow } from "../util/time";
import { readSuccessResponseJsonOrThrow } from "../util/http";
import { URL } from "../util/url";
export async function getTipStatus(
  ws: InternalWalletState,
  talerTipUri: string,
): Promise {
  const res = parseTipUri(talerTipUri);
  if (!res) {
    throw Error("invalid taler://tip URI");
  }
  const tipStatusUrl = new URL("tip-pickup", res.merchantBaseUrl);
  tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
  console.log("checking tip status from", tipStatusUrl.href);
  const merchantResp = await ws.http.get(tipStatusUrl.href);
  const tipPickupStatus = await readSuccessResponseJsonOrThrow(
    merchantResp,
    codecForTipPickupGetResponse(),
  );
  console.log("status", tipPickupStatus);
  const amount = Amounts.parseOrThrow(tipPickupStatus.amount);
  const merchantOrigin = new URL(res.merchantBaseUrl).origin;
  let tipRecord = await ws.db.get(Stores.tips, [
    res.merchantTipId,
    merchantOrigin,
  ]);
  if (!tipRecord) {
    await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
    const withdrawDetails = await getExchangeWithdrawalInfo(
      ws,
      tipPickupStatus.exchange_url,
      amount,
    );
    const tipId = encodeCrock(getRandomBytes(32));
    const selectedDenoms = await selectWithdrawalDenoms(
      ws,
      tipPickupStatus.exchange_url,
      amount,
    );
    tipRecord = {
      tipId,
      acceptedTimestamp: undefined,
      rejectedTimestamp: undefined,
      amount,
      deadline: tipPickupStatus.stamp_expire,
      exchangeUrl: tipPickupStatus.exchange_url,
      merchantBaseUrl: res.merchantBaseUrl,
      nextUrl: undefined,
      pickedUp: false,
      planchets: undefined,
      response: undefined,
      createdTimestamp: getTimestampNow(),
      merchantTipId: res.merchantTipId,
      totalFees: Amounts.add(
        withdrawDetails.overhead,
        withdrawDetails.withdrawFee,
      ).amount,
      retryInfo: initRetryInfo(),
      lastError: undefined,
      denomsSel: denomSelectionInfoToState(selectedDenoms),
    };
    await ws.db.put(Stores.tips, tipRecord);
  }
  const tipStatus: TipStatus = {
    accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
    amount: Amounts.parseOrThrow(tipPickupStatus.amount),
    amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
    exchangeUrl: tipPickupStatus.exchange_url,
    nextUrl: tipPickupStatus.extra.next_url,
    merchantOrigin: merchantOrigin,
    merchantTipId: res.merchantTipId,
    expirationTimestamp: tipPickupStatus.stamp_expire,
    timestamp: tipPickupStatus.stamp_created,
    totalFees: tipRecord.totalFees,
    tipId: tipRecord.tipId,
  };
  return tipStatus;
}
async function incrementTipRetry(
  ws: InternalWalletState,
  refreshSessionId: string,
  err: OperationErrorDetails | undefined,
): Promise {
  await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
    const t = await tx.get(Stores.tips, refreshSessionId);
    if (!t) {
      return;
    }
    if (!t.retryInfo) {
      return;
    }
    t.retryInfo.retryCounter++;
    updateRetryInfoTimeout(t.retryInfo);
    t.lastError = err;
    await tx.put(Stores.tips, t);
  });
  ws.notify({ type: NotificationType.TipOperationError });
}
export async function processTip(
  ws: InternalWalletState,
  tipId: string,
  forceNow = false,
): Promise {
  const onOpErr = (e: OperationErrorDetails): Promise =>
    incrementTipRetry(ws, tipId, e);
  await guardOperationException(
    () => processTipImpl(ws, tipId, forceNow),
    onOpErr,
  );
}
async function resetTipRetry(
  ws: InternalWalletState,
  tipId: string,
): Promise {
  await ws.db.mutate(Stores.tips, tipId, (x) => {
    if (x.retryInfo.active) {
      x.retryInfo = initRetryInfo();
    }
    return x;
  });
}
async function processTipImpl(
  ws: InternalWalletState,
  tipId: string,
  forceNow: boolean,
): Promise {
  if (forceNow) {
    await resetTipRetry(ws, tipId);
  }
  let tipRecord = await ws.db.get(Stores.tips, tipId);
  if (!tipRecord) {
    return;
  }
  if (tipRecord.pickedUp) {
    console.log("tip already picked up");
    return;
  }
  const denomsForWithdraw = tipRecord.denomsSel;
  if (!tipRecord.planchets) {
    const planchets: TipPlanchet[] = [];
    for (const sd of denomsForWithdraw.selectedDenoms) {
      const denom = await ws.db.getIndexed(
        Stores.denominations.denomPubHashIndex,
        sd.denomPubHash,
      );
      if (!denom) {
        throw Error("denom does not exist anymore");
      }
      for (let i = 0; i < sd.count; i++) {
        const r = await ws.cryptoApi.createTipPlanchet(denom);
        planchets.push(r);
      }
    }
    await ws.db.mutate(Stores.tips, tipId, (r) => {
      if (!r.planchets) {
        r.planchets = planchets;
      }
      return r;
    });
  }
  tipRecord = await ws.db.get(Stores.tips, tipId);
  if (!tipRecord) {
    throw Error("tip not in database");
  }
  if (!tipRecord.planchets) {
    throw Error("invariant violated");
  }
  console.log("got planchets for tip!");
  // Planchets in the form that the merchant expects
  const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({
    coin_ev: p.coinEv,
    denom_pub_hash: p.denomPubHash,
  }));
  let merchantResp;
  const tipStatusUrl = new URL("tip-pickup", tipRecord.merchantBaseUrl);
  try {
    const req = { planchets: planchetsDetail, tip_id: tipRecord.merchantTipId };
    merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
    if (merchantResp.status !== 200) {
      throw Error(`unexpected status ${merchantResp.status} for tip-pickup`);
    }
    console.log("got merchant resp:", merchantResp);
  } catch (e) {
    console.log("tipping failed", e);
    throw e;
  }
  const response = codecForTipResponse().decode(await merchantResp.json());
  if (response.reserve_sigs.length !== tipRecord.planchets.length) {
    throw Error("number of tip responses does not match requested planchets");
  }
  const withdrawalGroupId = encodeCrock(getRandomBytes(32));
  const planchets: PlanchetRecord[] = [];
  for (let i = 0; i < tipRecord.planchets.length; i++) {
    const tipPlanchet = tipRecord.planchets[i];
    const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv);
    const planchet: PlanchetRecord = {
      blindingKey: tipPlanchet.blindingKey,
      coinEv: tipPlanchet.coinEv,
      coinPriv: tipPlanchet.coinPriv,
      coinPub: tipPlanchet.coinPub,
      coinValue: tipPlanchet.coinValue,
      denomPub: tipPlanchet.denomPub,
      denomPubHash: tipPlanchet.denomPubHash,
      reservePub: response.reserve_pub,
      withdrawSig: response.reserve_sigs[i].reserve_sig,
      isFromTip: true,
      coinEvHash,
      coinIdx: i,
      withdrawalDone: false,
      withdrawalGroupId: withdrawalGroupId,
      lastError: undefined,
    };
    planchets.push(planchet);
  }
  const withdrawalGroup: WithdrawalGroupRecord = {
    exchangeBaseUrl: tipRecord.exchangeUrl,
    source: {
      type: WithdrawalSourceType.Tip,
      tipId: tipRecord.tipId,
    },
    timestampStart: getTimestampNow(),
    withdrawalGroupId: withdrawalGroupId,
    rawWithdrawalAmount: tipRecord.amount,
    retryInfo: initRetryInfo(),
    timestampFinish: undefined,
    lastError: undefined,
    denomsSel: tipRecord.denomsSel,
  };
  await ws.db.runWithWriteTransaction(
    [Stores.tips, Stores.withdrawalGroups],
    async (tx) => {
      const tr = await tx.get(Stores.tips, tipId);
      if (!tr) {
        return;
      }
      if (tr.pickedUp) {
        return;
      }
      tr.pickedUp = true;
      tr.retryInfo = initRetryInfo(false);
      await tx.put(Stores.tips, tr);
      await tx.put(Stores.withdrawalGroups, withdrawalGroup);
      for (const p of planchets) {
        await tx.put(Stores.planchets, p);
      }
    },
  );
  await processWithdrawGroup(ws, withdrawalGroupId);
}
export async function acceptTip(
  ws: InternalWalletState,
  tipId: string,
): Promise {
  const tipRecord = await ws.db.get(Stores.tips, tipId);
  if (!tipRecord) {
    console.log("tip not found");
    return;
  }
  tipRecord.acceptedTimestamp = getTimestampNow();
  await ws.db.put(Stores.tips, tipRecord);
  await processTip(ws, tipId);
  return;
}