/*
 This file is part of GNU Taler
 (C) 2020-2025 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 <http://www.gnu.org/licenses/>
 */

/**
 * Implementation of wallet backups (export/import/upload) and sync
 * server management.
 *
 * @author Florian Dold <dold@taler.net>
 */

/**
 * Imports.
 */
import {
  AbsoluteTime,
  AttentionType,
  BackupRecovery,
  Codec,
  Duration,
  EddsaKeyPair,
  HttpStatusCode,
  Logger,
  PreparePayResult,
  ProviderInfo,
  ProviderPaymentStatus,
  RecoveryLoadRequest,
  RecoveryMergeStrategy,
  TalerError,
  TalerErrorCode,
  TalerPreciseTimestamp,
  URL,
  buildCodecForObject,
  buildCodecForUnion,
  bytesToString,
  canonicalJson,
  checkDbInvariant,
  checkLogicInvariant,
  codecForBoolean,
  codecForConstString,
  codecForList,
  codecForString,
  codecForSyncTermsOfServiceResponse,
  codecOptional,
  decodeCrock,
  eddsaGetPublic,
  encodeCrock,
  getRandomBytes,
  hash,
  j2s,
  kdf,
  notEmpty,
  secretbox,
  secretbox_open,
  stringToBytes,
} from "@gnu-taler/taler-util";
import {
  readSuccessResponseJsonOrThrow,
  readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import { gunzipSync, gzipSync } from "fflate";
import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
import {
  cancelableFetch,
  TaskIdentifiers,
  TaskRunResult,
  TaskRunResultType,
} from "../common.js";
import {
  BackupProviderRecord,
  BackupProviderState,
  BackupProviderStateTag,
  ConfigRecord,
  ConfigRecordKey,
  WalletBackupConfState,
  WalletDbReadOnlyTransaction,
  timestampOptionalPreciseFromDb,
  timestampPreciseToDb,
} from "../db.js";
import { preparePayForUri } from "../pay-merchant.js";
import { InternalWalletState, WalletExecutionContext } from "../wallet.js";

const logger = new Logger("operations/backup.ts");

function concatArrays(xs: Uint8Array[]): Uint8Array {
  let len = 0;
  for (const x of xs) {
    len += x.byteLength;
  }
  const out = new Uint8Array(len);
  let offset = 0;
  for (const x of xs) {
    out.set(x, offset);
    offset += x.length;
  }
  return out;
}

const magic = "TLRWBK01";

/**
 * Encrypt the backup.
 *
 * Blob format:
 * Magic "TLRWBK01" (8 bytes)
 * Nonce (24 bytes)
 * Compressed JSON blob (rest)
 */
export async function encryptBackup(
  config: WalletBackupConfState,
  blob: any,
): Promise<Uint8Array> {
  const chunks: Uint8Array[] = [];
  chunks.push(stringToBytes(magic));
  const nonceStr = config.lastBackupNonce;
  checkLogicInvariant(!!nonceStr);
  const nonce = decodeCrock(nonceStr).slice(0, 24);
  chunks.push(nonce);
  const backupJsonContent = canonicalJson(blob);
  logger.trace("backup JSON size", backupJsonContent.length);
  const compressedContent = gzipSync(stringToBytes(backupJsonContent), {
    mtime: 0,
  });
  const secret = deriveBlobSecret(config);
  const encrypted = secretbox(compressedContent, nonce.slice(0, 24), secret);
  chunks.push(encrypted);
  return concatArrays(chunks);
}

function deriveAccountKeyPair(
  bc: WalletBackupConfState,
  providerUrl: string,
): EddsaKeyPair {
  const privateKey = kdf(
    32,
    decodeCrock(bc.walletRootPriv),
    stringToBytes("taler-sync-account-key-salt"),
    stringToBytes(providerUrl),
  );
  return {
    eddsaPriv: privateKey,
    eddsaPub: eddsaGetPublic(privateKey),
  };
}

function deriveBlobSecret(bc: WalletBackupConfState): Uint8Array {
  return kdf(
    32,
    decodeCrock(bc.walletRootPriv),
    stringToBytes("taler-sync-blob-secret-salt"),
    stringToBytes("taler-sync-blob-secret-info"),
  );
}

interface BackupForProviderArgs {
  backupProviderBaseUrl: string;
}

function getNextBackupTimestamp(): TalerPreciseTimestamp {
  // FIXME:  Randomize!
  return AbsoluteTime.toPreciseTimestamp(
    AbsoluteTime.addDuration(
      AbsoluteTime.now(),
      Duration.fromSpec({ minutes: 5 }),
    ),
  );
}

async function runBackupCycleForProvider(
  wex: WalletExecutionContext,
  args: BackupForProviderArgs,
): Promise<TaskRunResult> {
  const provider = await wex.db.runReadOnlyTx(
    { storeNames: ["backupProviders"] },
    async (tx) => {
      return tx.backupProviders.get(args.backupProviderBaseUrl);
    },
  );

  if (!provider) {
    logger.warn("provider disappeared");
    return TaskRunResult.finished();
  }

  //const backupJson = await exportBackup(ws);
  // FIXME: re-implement backup
  const backupJson = {};
  const backupConfig = await provideBackupState(wex);
  const encBackup = await encryptBackup(backupConfig, backupJson);
  const currentBackupHash = hash(encBackup);

  const accountKeyPair = deriveAccountKeyPair(backupConfig, provider.baseUrl);

  const newHash = encodeCrock(currentBackupHash);
  const oldHash = provider.lastBackupHash;

  logger.trace(`trying to upload backup to ${provider.baseUrl}`);
  logger.trace(`old hash ${oldHash}, new hash ${newHash}`);

  const syncSigResp = await wex.cryptoApi.makeSyncSignature({
    newHash: encodeCrock(currentBackupHash),
    oldHash: provider.lastBackupHash,
    accountPriv: encodeCrock(accountKeyPair.eddsaPriv),
  });

  logger.trace(`sync signature is ${syncSigResp}`);

  const accountBackupUrl = new URL(
    `/backups/${encodeCrock(accountKeyPair.eddsaPub)}`,
    provider.baseUrl,
  );

  if (provider.shouldRetryFreshProposal) {
    accountBackupUrl.searchParams.set("fresh", "yes");
  }

  const resp = await cancelableFetch(wex, accountBackupUrl, {
    method: "POST",
    body: encBackup,
    headers: {
      "content-type": "application/octet-stream",
      "sync-signature": syncSigResp.sig,
      "if-none-match": JSON.stringify(newHash),
      ...(provider.lastBackupHash
        ? {
            "if-match": JSON.stringify(provider.lastBackupHash),
          }
        : {}),
    },
  });

  logger.trace(`sync response status: ${resp.status}`);

  if (resp.status === HttpStatusCode.NotModified) {
    await wex.db.runReadWriteTx(
      { storeNames: ["backupProviders"] },
      async (tx) => {
        const prov = await tx.backupProviders.get(provider.baseUrl);
        if (!prov) {
          return;
        }
        prov.lastBackupCycleTimestamp = timestampPreciseToDb(
          TalerPreciseTimestamp.now(),
        );
        prov.state = {
          tag: BackupProviderStateTag.Ready,
          nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
        };
        await tx.backupProviders.put(prov);
      },
    );

    removeAttentionRequest(wex, {
      entityId: provider.baseUrl,
      type: AttentionType.BackupUnpaid,
    });

    return TaskRunResult.finished();
  }

  if (resp.status === HttpStatusCode.PaymentRequired) {
    logger.trace("payment required for backup");
    logger.trace(`headers: ${j2s(resp.headers)}`);
    const talerUri = resp.headers.get("taler");
    if (!talerUri) {
      throw Error("no taler URI available to pay provider");
    }

    //We can't delay downloading the proposal since we need the id
    //FIXME: check download errors
    let res: PreparePayResult | undefined = undefined;
    try {
      res = await preparePayForUri(wex, talerUri);
    } catch (e) {
      const error = TalerError.fromException(e);
      if (!error.hasErrorCode(TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED)) {
        throw error;
      }
    }

    if (res === undefined) {
      //claimed

      await wex.db.runReadWriteTx(
        { storeNames: ["backupProviders"] },
        async (tx) => {
          const prov = await tx.backupProviders.get(provider.baseUrl);
          if (!prov) {
            logger.warn("backup provider not found anymore");
            return;
          }
          prov.shouldRetryFreshProposal = true;
          prov.state = {
            tag: BackupProviderStateTag.Retrying,
          };
          await tx.backupProviders.put(prov);
        },
      );

      throw Error("not implemented");
      // return {
      //   type: TaskRunResultType.Pending,
      // };
    }
    const result = res;

    await wex.db.runReadWriteTx(
      { storeNames: ["backupProviders"] },
      async (tx) => {
        const prov = await tx.backupProviders.get(provider.baseUrl);
        if (!prov) {
          logger.warn("backup provider not found anymore");
          return;
        }
        // const opId = TaskIdentifiers.forBackup(prov);
        // await scheduleRetryInTx(ws, tx, opId);
        prov.currentPaymentTransactionId = result.transactionId;
        prov.shouldRetryFreshProposal = false;
        prov.state = {
          tag: BackupProviderStateTag.Retrying,
        };
        await tx.backupProviders.put(prov);
      },
    );

    addAttentionRequest(
      wex,
      {
        type: AttentionType.BackupUnpaid,
        provider_base_url: provider.baseUrl,
        talerUri,
      },
      provider.baseUrl,
    );

    throw Error("not implemented");
    // return {
    //   type: TaskRunResultType.Pending,
    // };
  }

  if (resp.status === HttpStatusCode.NoContent) {
    await wex.db.runReadWriteTx(
      { storeNames: ["backupProviders"] },
      async (tx) => {
        const prov = await tx.backupProviders.get(provider.baseUrl);
        if (!prov) {
          return;
        }
        prov.lastBackupHash = encodeCrock(currentBackupHash);
        prov.lastBackupCycleTimestamp = timestampPreciseToDb(
          TalerPreciseTimestamp.now(),
        );
        prov.state = {
          tag: BackupProviderStateTag.Ready,
          nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
        };
        await tx.backupProviders.put(prov);
      },
    );

    removeAttentionRequest(wex, {
      entityId: provider.baseUrl,
      type: AttentionType.BackupUnpaid,
    });

    return {
      type: TaskRunResultType.Finished,
    };
  }

  if (resp.status === HttpStatusCode.Conflict) {
    logger.info("conflicting backup found");
    const backupEnc = new Uint8Array(await resp.bytes());
    const backupConfig = await provideBackupState(wex);
    // const blob = await decryptBackup(backupConfig, backupEnc);
    // FIXME: Re-implement backup import with merging
    // await importBackup(ws, blob, cryptoData);
    await wex.db.runReadWriteTx(
      { storeNames: ["backupProviders"] },
      async (tx) => {
        const prov = await tx.backupProviders.get(provider.baseUrl);
        if (!prov) {
          logger.warn("backup provider not found anymore");
          return;
        }
        prov.lastBackupHash = encodeCrock(hash(backupEnc));
        // FIXME: Allocate error code for this situation?
        // FIXME: Add operation retry record!
        const opId = TaskIdentifiers.forBackup(prov);
        //await scheduleRetryInTx(ws, tx, opId);
        prov.state = {
          tag: BackupProviderStateTag.Retrying,
        };
        await tx.backupProviders.put(prov);
      },
    );
    logger.info("processed existing backup");
    // Now upload our own, merged backup.
    return await runBackupCycleForProvider(wex, args);
  }

  // Some other response that we did not expect!

  logger.error("parsing error response");

  const err = await readTalerErrorResponse(resp);
  logger.error(`got error response from backup provider: ${j2s(err)}`);
  return {
    type: TaskRunResultType.Error,
    errorDetail: err,
  };
}

export async function processBackupForProvider(
  wex: WalletExecutionContext,
  backupProviderBaseUrl: string,
): Promise<TaskRunResult> {
  if (!wex.ws.networkAvailable) {
    return TaskRunResult.networkRequired();
  }

  const provider = await wex.db.runReadOnlyTx(
    { storeNames: ["backupProviders"] },
    async (tx) => {
      return await tx.backupProviders.get(backupProviderBaseUrl);
    },
  );
  if (!provider) {
    throw Error("unknown backup provider");
  }

  logger.info(`running backup for provider ${backupProviderBaseUrl}`);

  return await runBackupCycleForProvider(wex, {
    backupProviderBaseUrl: provider.baseUrl,
  });
}

export interface RemoveBackupProviderRequest {
  provider: string;
}

export const codecForRemoveBackupProvider =
  (): Codec<RemoveBackupProviderRequest> =>
    buildCodecForObject<RemoveBackupProviderRequest>()
      .property("provider", codecForString())
      .build("RemoveBackupProviderRequest");

export async function removeBackupProvider(
  wex: WalletExecutionContext,
  req: RemoveBackupProviderRequest,
): Promise<void> {
  await wex.db.runReadWriteTx(
    { storeNames: ["backupProviders"] },
    async (tx) => {
      await tx.backupProviders.delete(req.provider);
    },
  );
}

export interface RunBackupCycleRequest {
  /**
   * List of providers to backup or empty for all known providers.
   */
  providers?: Array<string>;
}

export const codecForRunBackupCycle = (): Codec<RunBackupCycleRequest> =>
  buildCodecForObject<RunBackupCycleRequest>()
    .property("providers", codecOptional(codecForList(codecForString())))
    .build("RunBackupCycleRequest");

/**
 * Do one backup cycle that consists of:
 * 1. Exporting a backup and try to upload it.
 *    Stop if this step succeeds.
 * 2. Download, verify and import backups from connected sync accounts.
 * 3. Upload the updated backup blob.
 */
export async function runBackupCycle(
  wex: WalletExecutionContext,
  req: RunBackupCycleRequest,
): Promise<void> {
  const providers = await wex.db.runReadOnlyTx(
    { storeNames: ["backupProviders"] },
    async (tx) => {
      if (req.providers) {
        const rs = await Promise.all(
          req.providers.map((id) => tx.backupProviders.get(id)),
        );
        return rs.filter(notEmpty);
      }
      return await tx.backupProviders.iter().toArray();
    },
  );

  for (const provider of providers) {
    await runBackupCycleForProvider(wex, {
      backupProviderBaseUrl: provider.baseUrl,
    });
  }
}

export interface AddBackupProviderRequest {
  backupProviderBaseUrl: string;

  name: string;
  /**
   * Activate the provider.  Should only be done after
   * the user has reviewed the provider.
   */
  activate?: boolean;
}

export const codecForAddBackupProviderRequest =
  (): Codec<AddBackupProviderRequest> =>
    buildCodecForObject<AddBackupProviderRequest>()
      .property("backupProviderBaseUrl", codecForString())
      .property("name", codecForString())
      .property("activate", codecOptional(codecForBoolean()))
      .build("AddBackupProviderRequest");

export type AddBackupProviderResponse =
  | AddBackupProviderOk
  | AddBackupProviderPaymentRequired;

interface AddBackupProviderOk {
  status: "ok";
}
interface AddBackupProviderPaymentRequired {
  status: "payment-required";
  talerUri?: string;
}

export const codecForAddBackupProviderOk = (): Codec<AddBackupProviderOk> =>
  buildCodecForObject<AddBackupProviderOk>()
    .property("status", codecForConstString("ok"))
    .build("AddBackupProviderOk");

export const codecForAddBackupProviderPaymenrRequired =
  (): Codec<AddBackupProviderPaymentRequired> =>
    buildCodecForObject<AddBackupProviderPaymentRequired>()
      .property("status", codecForConstString("payment-required"))
      .property("talerUri", codecOptional(codecForString()))
      .build("AddBackupProviderPaymentRequired");

export const codecForAddBackupProviderResponse =
  (): Codec<AddBackupProviderResponse> =>
    buildCodecForUnion<AddBackupProviderResponse>()
      .discriminateOn("status")
      .alternative("ok", codecForAddBackupProviderOk())
      .alternative(
        "payment-required",
        codecForAddBackupProviderPaymenrRequired(),
      )
      .build("AddBackupProviderResponse");

export async function addBackupProvider(
  wex: WalletExecutionContext,
  req: AddBackupProviderRequest,
): Promise<AddBackupProviderResponse> {
  logger.info(`adding backup provider ${j2s(req)}`);
  await provideBackupState(wex);
  const canonUrl = req.backupProviderBaseUrl;
  await wex.db.runReadWriteTx(
    { storeNames: ["backupProviders"] },
    async (tx) => {
      const oldProv = await tx.backupProviders.get(canonUrl);
      if (oldProv) {
        logger.info("old backup provider found");
        if (req.activate) {
          oldProv.state = {
            tag: BackupProviderStateTag.Ready,
            nextBackupTimestamp: timestampPreciseToDb(
              TalerPreciseTimestamp.now(),
            ),
          };
          logger.info("setting existing backup provider to active");
          await tx.backupProviders.put(oldProv);
        }
        return;
      }
    },
  );
  const termsUrl = new URL("config", canonUrl);
  const resp = await cancelableFetch(wex, termsUrl);
  const terms = await readSuccessResponseJsonOrThrow(
    resp,
    codecForSyncTermsOfServiceResponse(),
  );
  await wex.db.runReadWriteTx(
    { storeNames: ["backupProviders"] },
    async (tx) => {
      let state: BackupProviderState;
      //FIXME: what is the difference provisional and ready?
      if (req.activate) {
        state = {
          tag: BackupProviderStateTag.Ready,
          nextBackupTimestamp: timestampPreciseToDb(
            TalerPreciseTimestamp.now(),
          ),
        };
      } else {
        state = {
          tag: BackupProviderStateTag.Provisional,
        };
      }
      await tx.backupProviders.put({
        state,
        name: req.name,
        terms: {
          annualFee: terms.annual_fee,
          storageLimitInMegabytes: terms.storage_limit_in_megabytes,
          supportedProtocolVersion: terms.version,
        },
        shouldRetryFreshProposal: false,
        paymentProposalIds: [],
        baseUrl: canonUrl,
        uids: [encodeCrock(getRandomBytes(32))],
      });
    },
  );

  return await runFirstBackupCycleForProvider(wex, {
    backupProviderBaseUrl: canonUrl,
  });
}

async function runFirstBackupCycleForProvider(
  wex: WalletExecutionContext,
  args: BackupForProviderArgs,
): Promise<AddBackupProviderResponse> {
  throw Error("not implemented");
  // const resp = await runBackupCycleForProvider(ws, args);
  // switch (resp.type) {
  //   case TaskRunResultType.Error:
  //     throw TalerError.fromDetail(
  //       TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
  //       resp.errorDetail as any, //FIXME create an error for backup problems
  //     );
  //   case TaskRunResultType.Finished:
  //     return {
  //       status: "ok",
  //     };
  //   case TaskRunResultType.Pending:
  //     return {
  //       status: "payment-required",
  //       talerUri: "FIXME",
  //       //talerUri: resp.result.talerUri,
  //     };
  //   default:
  //     assertUnreachable(resp);
  // }
}

export async function restoreFromRecoverySecret(): Promise<void> {
  return;
}

export interface BackupInfo {
  walletRootPub: string;
  deviceId: string;
  providers: ProviderInfo[];
}

async function getProviderPaymentInfo(
  wex: WalletExecutionContext,
  provider: BackupProviderRecord,
): Promise<ProviderPaymentStatus> {
  throw Error("not implemented");
  // if (!provider.currentPaymentProposalId) {
  //   return {
  //     type: ProviderPaymentType.Unpaid,
  //   };
  // }
  // const status = await checkPaymentByProposalId(
  //   ws,
  //   provider.currentPaymentProposalId,
  // ).catch(() => undefined);

  // if (!status) {
  //   return {
  //     type: ProviderPaymentType.Unpaid,
  //   };
  // }

  // switch (status.status) {
  //   case PreparePayResultType.InsufficientBalance:
  //     return {
  //       type: ProviderPaymentType.InsufficientBalance,
  //       amount: status.amountRaw,
  //     };
  //   case PreparePayResultType.PaymentPossible:
  //     return {
  //       type: ProviderPaymentType.Pending,
  //       talerUri: status.talerUri,
  //     };
  //   case PreparePayResultType.AlreadyConfirmed:
  //     if (status.paid) {
  //       return {
  //         type: ProviderPaymentType.Paid,
  //         paidUntil: AbsoluteTime.addDuration(
  //           AbsoluteTime.fromProtocolTimestamp(status.contractTerms.timestamp),
  //           durationFromSpec({ years: 1 }), //FIXME: take this from the contract term
  //         ),
  //       };
  //     } else {
  //       return {
  //         type: ProviderPaymentType.Pending,
  //         talerUri: status.talerUri,
  //       };
  //     }
  //   default:
  //     assertUnreachable(status);
  // }
}

/**
 * Get information about the current state of wallet backups.
 */
export async function getBackupInfo(
  wex: WalletExecutionContext,
): Promise<BackupInfo> {
  const backupConfig = await provideBackupState(wex);
  const providerRecords = await wex.db.runReadOnlyTx(
    { storeNames: ["backupProviders", "operationRetries"] },
    async (tx) => {
      return await tx.backupProviders.iter().mapAsync(async (bp) => {
        const opId = TaskIdentifiers.forBackup(bp);
        const retryRecord = await tx.operationRetries.get(opId);
        return {
          provider: bp,
          retryRecord,
        };
      });
    },
  );
  const providers: ProviderInfo[] = [];
  for (const x of providerRecords) {
    providers.push({
      active: x.provider.state.tag !== BackupProviderStateTag.Provisional,
      syncProviderBaseUrl: x.provider.baseUrl,
      lastSuccessfulBackupTimestamp: timestampOptionalPreciseFromDb(
        x.provider.lastBackupCycleTimestamp,
      ),
      paymentProposalIds: x.provider.paymentProposalIds,
      lastError:
        x.provider.state.tag === BackupProviderStateTag.Retrying
          ? x.retryRecord?.lastError
          : undefined,
      paymentStatus: await getProviderPaymentInfo(wex, x.provider),
      terms: x.provider.terms,
      name: x.provider.name,
    });
  }
  return {
    deviceId: backupConfig.deviceId,
    walletRootPub: backupConfig.walletRootPub,
    providers,
  };
}

/**
 * Get backup recovery information, including the wallet's
 * private key.
 */
export async function getBackupRecovery(
  wex: WalletExecutionContext,
): Promise<BackupRecovery> {
  const bs = await provideBackupState(wex);
  const providers = await wex.db.runReadOnlyTx(
    { storeNames: ["backupProviders"] },
    async (tx) => {
      return await tx.backupProviders.iter().toArray();
    },
  );
  return {
    providers: providers
      .filter((x) => x.state.tag !== BackupProviderStateTag.Provisional)
      .map((x) => {
        return {
          name: x.name,
          url: x.baseUrl,
        };
      }),
    walletRootPriv: bs.walletRootPriv,
  };
}

async function backupRecoveryTheirs(
  wex: WalletExecutionContext,
  br: BackupRecovery,
) {
  await wex.db.runReadWriteTx(
    { storeNames: ["backupProviders", "config"] },
    async (tx) => {
      let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
        ConfigRecordKey.WalletBackupState,
      );
      checkDbInvariant(!!backupStateEntry, `no backup entry`);
      checkDbInvariant(
        backupStateEntry.key === ConfigRecordKey.WalletBackupState,
        `backup entry inconsistent`,
      );
      backupStateEntry.value.lastBackupNonce = undefined;
      backupStateEntry.value.lastBackupTimestamp = undefined;
      backupStateEntry.value.lastBackupCheckTimestamp = undefined;
      backupStateEntry.value.lastBackupPlainHash = undefined;
      backupStateEntry.value.walletRootPriv = br.walletRootPriv;
      backupStateEntry.value.walletRootPub = encodeCrock(
        eddsaGetPublic(decodeCrock(br.walletRootPriv)),
      );
      await tx.config.put(backupStateEntry);
      for (const prov of br.providers) {
        const existingProv = await tx.backupProviders.get(prov.url);
        if (!existingProv) {
          await tx.backupProviders.put({
            baseUrl: prov.url,
            name: prov.name,
            paymentProposalIds: [],
            shouldRetryFreshProposal: false,
            state: {
              tag: BackupProviderStateTag.Ready,
              nextBackupTimestamp: timestampPreciseToDb(
                TalerPreciseTimestamp.now(),
              ),
            },
            uids: [encodeCrock(getRandomBytes(32))],
          });
        }
      }
      const providers = await tx.backupProviders.iter().toArray();
      for (const prov of providers) {
        prov.lastBackupCycleTimestamp = undefined;
        prov.lastBackupHash = undefined;
        await tx.backupProviders.put(prov);
      }
    },
  );
}

async function backupRecoveryOurs(
  wex: WalletExecutionContext,
  br: BackupRecovery,
) {
  throw Error("not implemented");
}

export async function loadBackupRecovery(
  wex: WalletExecutionContext,
  br: RecoveryLoadRequest,
): Promise<void> {
  const bs = await provideBackupState(wex);
  const providers = await wex.db.runReadOnlyTx(
    { storeNames: ["backupProviders"] },
    async (tx) => {
      return await tx.backupProviders.iter().toArray();
    },
  );
  let strategy = br.strategy;
  if (
    br.recovery.walletRootPriv != bs.walletRootPriv &&
    providers.length > 0 &&
    !strategy
  ) {
    throw Error(
      "recovery load strategy must be specified for wallet with existing providers",
    );
  } else if (!strategy) {
    // Default to using the new key if we don't have providers yet.
    strategy = RecoveryMergeStrategy.Theirs;
  }
  if (strategy === RecoveryMergeStrategy.Theirs) {
    return backupRecoveryTheirs(wex, br.recovery);
  } else {
    return backupRecoveryOurs(wex, br.recovery);
  }
}

export async function decryptBackup(
  backupConfig: WalletBackupConfState,
  data: Uint8Array,
): Promise<any> {
  const rMagic = bytesToString(data.slice(0, 8));
  if (rMagic != magic) {
    throw Error("invalid backup file (magic tag mismatch)");
  }

  const nonce = data.slice(8, 8 + 24);
  const box = data.slice(8 + 24);
  const secret = deriveBlobSecret(backupConfig);
  const dataCompressed = secretbox_open(box, nonce, secret);
  if (!dataCompressed) {
    throw Error("decryption failed");
  }
  return JSON.parse(bytesToString(gunzipSync(dataCompressed)));
}

export async function provideBackupState(
  wex: WalletExecutionContext,
): Promise<WalletBackupConfState> {
  const bs: ConfigRecord | undefined = await wex.db.runReadOnlyTx(
    { storeNames: ["config"] },
    async (tx) => {
      return await tx.config.get(ConfigRecordKey.WalletBackupState);
    },
  );
  if (bs) {
    checkDbInvariant(
      bs.key === ConfigRecordKey.WalletBackupState,
      `backup entry inconsistent`,
    );
    return bs.value;
  }
  // We need to generate the key outside of the transaction
  // due to how IndexedDB works.
  const k = await wex.cryptoApi.createEddsaKeypair({});
  const d = getRandomBytes(5);
  // FIXME: device ID should be configured when wallet is initialized
  // and be based on hostname
  const deviceId = `wallet-core-${encodeCrock(d)}`;
  return await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
    let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
      ConfigRecordKey.WalletBackupState,
    );
    if (!backupStateEntry) {
      backupStateEntry = {
        key: ConfigRecordKey.WalletBackupState,
        value: {
          deviceId,
          walletRootPub: k.pub,
          walletRootPriv: k.priv,
          lastBackupPlainHash: undefined,
        },
      };
      await tx.config.put(backupStateEntry);
    }
    checkDbInvariant(
      backupStateEntry.key === ConfigRecordKey.WalletBackupState,
      `backup entry inconsistent`,
    );
    return backupStateEntry.value;
  });
}

export async function getWalletBackupState(
  ws: InternalWalletState,
  tx: WalletDbReadOnlyTransaction<["config"]>,
): Promise<WalletBackupConfState> {
  const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
  checkDbInvariant(!!bs, "wallet backup state should be in DB");
  checkDbInvariant(
    bs.key === ConfigRecordKey.WalletBackupState,
    `backup entry inconsistent`,
  );
  return bs.value;
}

export async function setWalletDeviceId(
  wex: WalletExecutionContext,
  deviceId: string,
): Promise<void> {
  await provideBackupState(wex);
  await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
    const backupStateEntry: ConfigRecord | undefined = await tx.config.get(
      ConfigRecordKey.WalletBackupState,
    );
    if (
      !backupStateEntry ||
      backupStateEntry.key !== ConfigRecordKey.WalletBackupState
    ) {
      return;
    }
    backupStateEntry.value.deviceId = deviceId;
    await tx.config.put(backupStateEntry);
  });
}

export async function getWalletDeviceId(
  wex: WalletExecutionContext,
): Promise<string> {
  const bs = await provideBackupState(wex);
  return bs.deviceId;
}
