<template>
  <QuickNav selected="Profile"></QuickNav>
  <TopMessage />

  <!-- CONTENT -->
  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
    <!-- Heading -->
    <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
      Your Identity
    </h1>

    <div class="flex justify-between">
      <span />
      <span class="whitespace-nowrap">
        <router-link
          :to="{ name: 'contact-qr' }"
          class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md"
        >
          <fa icon="qrcode" class="fa-fw"></fa>
        </router-link>
      </span>
      <span />
    </div>

    <div class="flex justify-between py-2">
      <span />
      <span>
        <router-link
          :to="{ name: 'help' }"
          class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
        >
          Help
        </router-link>
      </span>
    </div>

    <!-- ID notice -->
    <div
      v-if="!activeDid"
      class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
    >
      <p class="mb-4">
        <b>Note:</b> Before you can take any action, you need an ID.
      </p>
      <router-link
        :to="{ name: 'start' }"
        class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
      >
        Generate Identity
      </router-link>
    </div>

    <!-- Registration notice -->
    <!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
    <div
      v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
      class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
    >
      <p class="mb-4">
        <b>Note:</b> Before you can publicly announce a new project or time
        commitment, a friend needs to register you.
      </p>
      <router-link
        :to="{ name: 'contact-qr' }"
        class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
      >
        Share Your Info
      </router-link>
    </div>

    <!-- Identity Details -->
    <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
      <h2 v-if="givenName" class="text-xl font-semibold mb-2">
        {{ givenName }}
        <router-link :to="{ name: 'new-edit-account' }">
          <fa icon="pen" class="text-xs text-blue-500 mb-1"></fa>
        </router-link>
      </h2>
      <span v-else>
        <router-link
          :to="{ name: 'new-edit-account' }"
          class="block w-full text-center text-md text-blue-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
        >
          (Set Your Name)
        </router-link>
      </span>

      <div class="text-slate-500 text-sm font-bold">ID</div>
      <div class="text-sm text-slate-500 flex justify-start items-center mb-1">
        <code class="truncate">{{ activeDid }}</code>
        <button
          @click="
            doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
          "
          class="ml-2"
        >
          <fa icon="copy" class="text-slate-400 fa-fw"></fa>
        </button>
        <span v-show="showDidCopy">Copied!</span>
      </div>
    </div>

    <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
      <div
        v-if="!notificationMaybeChanged"
        class="flex items-center justify-between cursor-pointer"
        @click="showNotificationChoice()"
      >
        <!-- label -->
        <div>App Notifications</div>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input
            type="checkbox"
            v-model="isSubscribed"
            name="toggleNotificationsInput"
            class="sr-only"
          />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          ></div>
        </div>
      </div>
      <div v-else>
        Notification status may have changed. Refresh this page to see the
        latest setting.
      </div>
      <router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
        Troubleshoot your notification setup.
      </router-link>
    </div>

    <h3 class="text-sm uppercase font-semibold mb-3">Data Export</h3>

    <router-link
      :to="{ name: 'seed-backup' }"
      v-if="activeDid"
      class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
    >
      Backup Identifier Seed
    </router-link>

    <button
      v-bind:class="computedStartDownloadLinkClassNames()"
      class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
      @click="exportDatabase()"
    >
      Download Settings & Contacts
      <br />
      (excluding Identifier Data)
    </button>
    <a
      ref="downloadLink"
      v-bind:class="computedDownloadLinkClassNames()"
      class="block w-full text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
    >
      If no download happened yet, click again here to download now.
    </a>

    <div v-if="activeDid" class="flex mt-8 py-2">
      <h3 class="text-sm uppercase font-semibold">Rate Limits</h3>
      <button
        class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md ml-2 mr-2 mb-2"
        @click="checkLimits()"
      >
        Check Limits
      </button>
      <!-- show spinner if loading limits -->
      <div v-if="loadingLimits" class="text-center">
        Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
      </div>
      <div>
        {{ limitsMessage }}
      </div>
      <div v-if="!!limits?.nextWeekBeginDateTime">
        <p class="mb-3 text-sm">
          You have done <b>{{ limits.doneClaimsThisWeek }}</b> claims out of
          <b>{{ limits.maxClaimsPerWeek }}</b> for this week. Your claims
          counter resets at
          <b class="whitespace-nowrap">{{
            readableTime(limits.nextWeekBeginDateTime)
          }}</b>
        </p>
        <p class="text-sm">
          You have done
          <b>{{ limits.doneRegistrationsThisMonth }}</b> registrations out of
          <b>{{ limits.maxRegistrationsPerMonth }}</b> for this month.
          <i
            >(You can register nobody on your first day, and after that only one
            a day in your first month.)</i
          >
          Your registration counter resets at
          <b class="whitespace-nowrap">
            {{ readableTime(limits.nextMonthBeginDateTime) }}
          </b>
        </p>
      </div>
    </div>

    <!-- id used by puppeteer test script -->
    <h3
      id="advanced"
      class="text-sm uppercase font-semibold mb-3"
      @click="showAdvanced = !showAdvanced"
    >
      Advanced
    </h3>
    <div v-if="showAdvanced">
      <p class="text-rose-600 mb-8">
        Beware: the features here can be confusing and even change data in ways
        you do not expect. But we support your freedom!
      </p>

      <!-- Deep Identity Details -->
      <h2 class="text-sm uppercase font-semibold mb-3">
        Deep Identity Details
      </h2>
      <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
        <div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
        <div
          class="text-sm text-slate-500 flex justify-start items-center mb-1"
        >
          <code class="truncate">{{ publicBase64 }}</code>
          <button
            @click="
              doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
            "
            class="ml-2"
          >
            <fa icon="copy" class="text-slate-400 fa-fw"></fa>
          </button>
          <span v-show="showB64Copy">Copied!</span>
        </div>

        <div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
        <div
          class="text-sm text-slate-500 flex justify-start items-center mb-1"
        >
          <code class="truncate">{{ publicHex }}</code>
          <button
            @click="
              doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
            "
            class="ml-2"
          >
            <fa icon="copy" class="text-slate-400 fa-fw"></fa>
          </button>
          <span v-show="showPubCopy">Copied!</span>
        </div>

        <div class="text-slate-500 text-sm font-bold">Derivation Path</div>
        <div
          class="text-sm text-slate-500 flex justify-start items-center mb-1"
        >
          <code class="truncate">{{ derivationPath }}</code>
          <button
            @click="
              doCopyTwoSecRedo(
                derivationPath,
                () => (showDerCopy = !showDerCopy),
              )
            "
            class="ml-2"
          >
            <fa icon="copy" class="text-slate-400 fa-fw"></fa>
          </button>
          <span v-show="showDerCopy">Copied!</span>
        </div>
      </div>

      <label
        for="toggleShowAmounts"
        class="flex items-center justify-between cursor-pointer my-4"
        @click="handleChange"
      >
        <!-- label -->
        <h2>Show amounts given with contacts</h2>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input
            type="checkbox"
            v-model="showContactGives"
            name="showContactGives"
            class="sr-only"
          />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          ></div>
        </div>
      </label>

      <div class="grid-cols-2 mb-4">
        <span class="text-slate-500 text-sm font-bold mb-2">Data Import</span>
        <input type="file" @change="uploadFile" class="ml-2" />
        <div v-if="showContactImport()">
          <button
            class="block text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
            @click="submitFile()"
          >
            Import Settings & Contacts
            <br />
            (excluding Identifier Data)
          </button>
        </div>
      </div>

      <div class="flex py-2">
        <button>
          <router-link
            :to="{ name: 'statistics' }"
            class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
          >
            See Global Animated History of Giving
          </router-link>
        </button>
      </div>

      <!-- id used by puppeteer test script -->
      <router-link
        id="switch-identity-link"
        :to="{ name: 'identity-switcher' }"
        class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
      >
        Switch Identity
      </router-link>

      <div class="flex py-4">
        <h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
        <input
          type="text"
          class="block w-full rounded border border-slate-400 px-3 py-2"
          v-model="apiServerInput"
        />
        <button
          v-if="apiServerInput != apiServer"
          class="px-4 rounded bg-red-500 border border-slate-400"
          @click="onClickSaveApiServer()"
        >
          <fa icon="floppy-disk" class="fa-fw" color="white"></fa>
        </button>
        <button
          class="px-3 rounded bg-slate-200 border border-slate-400"
          @click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
        >
          Use Prod
        </button>
        <button
          class="px-3 rounded bg-slate-200 border border-slate-400"
          @click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
        >
          Use Test
        </button>
        <button
          class="px-3 rounded bg-slate-200 border border-slate-400"
          @click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
        >
          Use Local
        </button>
      </div>

      <label
        for="toggleProdWarningMessage"
        class="flex items-center justify-between cursor-pointer my-4"
        @click="toggleProdWarning"
      >
        <!-- label -->
        <h2>Show warning if on prod server</h2>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          ></div>
        </div>
      </label>

      <label
        for="toggleTestWarningMessage"
        class="flex items-center justify-between cursor-pointer my-4"
        @click="toggleTestWarning"
      >
        <!-- label -->
        <h2>Show warning if on non-prod server</h2>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          ></div>
        </div>
      </label>

      <div class="flex py-4">
        <h2 class="text-slate-500 text-sm font-bold mb-2">
          Notification Push Server
        </h2>
        <input
          type="text"
          class="block w-full rounded border border-slate-400 px-3 py-2"
          v-model="webPushServerInput"
        />
        <button
          v-if="webPushServerInput != webPushServer"
          class="px-4 rounded bg-red-500 border border-slate-400"
          @click="onClickSavePushServer()"
        >
          <fa icon="floppy-disk" class="fa-fw" color="white"></fa>
        </button>
        <button
          class="px-3 rounded bg-slate-200 border border-slate-400"
          @click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
        >
          Use Prod
        </button>
        <button
          class="px-3 rounded bg-slate-200 border border-slate-400"
          @click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
        >
          Use Test 1
        </button>
        <button
          class="px-3 rounded bg-slate-200 border border-slate-400"
          @click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
        >
          Use Test 2
        </button>
      </div>
      <span class="px-4 text-sm" v-if="!webPushServerInput">
        When that setting is blank, this app will use the default web push
        server URL:
        {{ AppConstants.DEFAULT_PUSH_SERVER }}
      </span>
    </div>
  </section>
</template>

<script lang="ts">
import { AxiosError, AxiosRequestConfig } from "axios";
import Dexie from "dexie";
import "dexie-export-import";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";

import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
import { ImportProgress } from "dexie-export-import/dist/import";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;

interface Notification {
  group: string;
  type: string;
  title: string;
  text: string;
}

interface IAccount {
  did: string;
  publicKeyHex: string;
  privateHex?: string;
  derivationPath: string;
}

const inputFileNameRef = ref<Blob>();

@Component({ components: { QuickNav, TopMessage } })
export default class AccountViewView extends Vue {
  $notify!: (notification: Notification, timeout?: number) => void;

  AppConstants = AppString;

  activeDid = "";
  apiServer = "";
  apiServerInput = "";
  derivationPath = "";
  downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
  givenName = "";
  isRegistered = false;
  isSubscribed = false;
  notificationMaybeChanged = false;
  numAccounts = 0;
  publicHex = "";
  publicBase64 = "";
  webPushServer = "";
  webPushServerInput = "";
  limits: RateLimits | null = null;
  limitsMessage = "";
  loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
  showContactGives = false;

  showDidCopy = false;
  showDerCopy = false;
  showB64Copy = false;
  showPubCopy = false;

  showAdvanced = false;

  subscription: PushSubscription | null = null;
  warnIfProdServer = false;
  warnIfTestServer = false;

  async beforeCreate() {
    await accountsDB.open();
    this.numAccounts = await accountsDB.accounts.count();
  }

  /**
   * Async function executed when the component is created.
   * Initializes the component's state with values from the database,
   * handles identity-related tasks, and checks limitations.
   *
   * @throws Will display specific messages to the user based on different errors.
   */
  async created() {
    try {
      await db.open();

      const settings = await db.settings.get(MASTER_SETTINGS_KEY);

      // Initialize component state with values from the database or defaults
      this.initializeState(settings);

      // Get and process the identity
      const identity = await this.getIdentity(this.activeDid);
      if (identity) {
        this.processIdentity(identity);
      }
    } catch (err: unknown) {
      this.handleError(err);
    }
  }

  async mounted() {
    try {
      const registration = await navigator.serviceWorker.ready;
      this.subscription = await registration.pushManager.getSubscription();
      this.isSubscribed = !!this.subscription;
    } catch (error) {
      console.error("Mount error:", error);
    }
  }

  beforeUnmount() {
    if (this.downloadUrl) {
      URL.revokeObjectURL(this.downloadUrl);
    }
  }

  /**
   * Initializes component state with values from the database or defaults.
   * @param {SettingsType} settings - Object containing settings from the database.
   */
  initializeState(settings: Settings | undefined) {
    this.activeDid = (settings?.activeDid as string) || "";
    this.apiServer = (settings?.apiServer as string) || "";
    this.apiServerInput = (settings?.apiServer as string) || "";
    this.givenName =
      (settings?.firstName || "") +
      (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
    this.isRegistered = !!settings?.isRegistered;
    this.showContactGives = !!settings?.showContactGivesInline;
    this.warnIfProdServer = !!settings?.warnIfProdServer;
    this.warnIfTestServer = !!settings?.warnIfTestServer;
    this.webPushServer = (settings?.webPushServer as string) || "";
    this.webPushServerInput = (settings?.webPushServer as string) || "";
  }

  public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
    try {
      // Open the accounts database
      await accountsDB.open();

      // Search for the account with the matching DID (decentralized identifier)
      const account: { identity?: string } | undefined =
        await accountsDB.accounts.where("did").equals(activeDid).first();

      // Return parsed identity or null if not found
      return JSON.parse((account?.identity as string) || "null");
    } catch (error) {
      console.error("Failed to find account:", error);
      return null;
    }
  }

  /**
   * Asynchronously retrieves headers for HTTP requests.
   *
   * @param {IIdentifier} identity - The identity object for which to generate the headers.
   * @returns {Promise<Record<string,string>>} A Promise that resolves to an object containing the headers.
   *
   * @throws Will throw an error if unable to generate an access token.
   */
  public async getHeaders(
    identity: IIdentifier,
  ): Promise<Record<string, string>> {
    try {
      const token = await accessToken(identity);

      const headers: Record<string, string> = {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      };

      return headers;
    } catch (error) {
      console.error("Failed to get headers:", error);
      return Promise.reject(error);
    }
  }

  // call fn, copy text to the clipboard, then redo fn after 2 seconds
  doCopyTwoSecRedo(text: string, fn: () => void) {
    fn();
    useClipboard()
      .copy(text)
      .then(() => setTimeout(fn, 2000));
  }

  handleChange() {
    this.showContactGives = !this.showContactGives;
    this.updateShowContactAmounts();
  }

  toggleProdWarning() {
    this.warnIfProdServer = !this.warnIfProdServer;
    this.updateWarnIfProdServer(this.warnIfProdServer);
  }

  toggleTestWarning() {
    this.warnIfTestServer = !this.warnIfTestServer;
    this.updateWarnIfTestServer(this.warnIfTestServer);
  }

  readableTime(timeStr: string) {
    return timeStr.substring(0, timeStr.indexOf("T"));
  }

  /**
   * Processes the identity and updates the component's state.
   * @param {IdentityType} identity - Object containing identity information.
   */
  processIdentity(identity: IIdentifier) {
    if (
      identity &&
      identity.keys &&
      identity.keys.length > 0 &&
      identity.keys[0].meta
    ) {
      this.publicHex = identity.keys[0].publicKeyHex;
      this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
      this.derivationPath = identity.keys[0].meta.derivationPath as string;

      db.settings.update(MASTER_SETTINGS_KEY, {
        activeDid: identity.did,
      });
      this.checkLimitsFor(identity);
    } else {
      // Handle the case where any of these are null or undefined
    }
  }

  async showNotificationChoice() {
    if (!this.subscription) {
      this.$notify(
        {
          group: "modal",
          type: "notification-permission",
          title: "", // unused, only here to satisfy type check
          text: "", // unused, only here to satisfy type check
        },
        -1,
      );
    } else {
      this.$notify(
        {
          group: "modal",
          type: "notification-off",
          title: "", // unused, only here to satisfy type check
          text: "", // unused, only here to satisfy type check
        },
        -1,
      );
    }
    this.notificationMaybeChanged = true;
  }

  /**
   * Handles errors and updates the component's state accordingly.
   * @param {Error} err - The error object.
   */
  handleError(err: unknown) {
    if (
      err instanceof Error &&
      err.message ===
        "Attempted to load account records with no identity available."
    ) {
      this.limitsMessage = "No identity.";
      this.loadingLimits = false;
    } else {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Creating Account",
          text: "Clear your cache and start over (after data backup).",
        },
        -1,
      );
      console.error("Telling user to clear cache at page create because:", err);
    }
  }

  public async updateShowContactAmounts() {
    try {
      await db.open();
      db.settings.update(MASTER_SETTINGS_KEY, {
        showContactGivesInline: this.showContactGives,
      });
    } catch (err) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Updating Contact Setting",
          text: "The setting may not have saved. Try again, maybe after restarting the app.",
        },
        -1,
      );
      console.error(
        "Telling user to try again after contact setting update because:",
        err,
      );
    }
  }

  public async updateWarnIfProdServer(newSetting: boolean) {
    try {
      await db.open();
      db.settings.update(MASTER_SETTINGS_KEY, {
        warnIfProdServer: newSetting,
      });
    } catch (err) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Updating Prod Warning",
          text: "The setting may not have saved. Try again, maybe after restarting the app.",
        },
        -1,
      );
      console.error(
        "Telling user to try again after setting update because:",
        err,
      );
    }
  }

  public async updateWarnIfTestServer(newSetting: boolean) {
    try {
      await db.open();
      db.settings.update(MASTER_SETTINGS_KEY, {
        warnIfTestServer: newSetting,
      });
    } catch (err) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Updating Test Warning",
          text: "The setting may not have saved. Try again, maybe after restarting the app.",
        },
        -1,
      );
      console.error(
        "Telling user to try again after setting update because:",
        err,
      );
    }
  }

  /**
   * Asynchronously exports the database into a downloadable JSON file.
   *
   * @throws Will notify the user if there is an export error.
   */
  public async exportDatabase() {
    try {
      // Generate the blob from the database
      const blob = await this.generateDatabaseBlob();

      // Create a temporary URL for the blob
      this.downloadUrl = this.createBlobURL(blob);

      // Trigger the download
      this.downloadDatabaseBackup(this.downloadUrl);

      // Revoke the temporary URL -- not yet because of DuckDuckGo download failure
      //URL.revokeObjectURL(this.downloadUrl);

      // Notify the user that the download has started
      this.notifyDownloadStarted();
    } catch (error) {
      this.handleExportError(error);
    }
  }

  /**
   * Generates a blob object representing the database.
   *
   * @returns {Promise<Blob>} The generated blob object.
   */
  private async generateDatabaseBlob(): Promise<Blob> {
    return await db.export({ prettyJson: true });
  }

  /**
   * Creates a temporary URL for a blob object.
   *
   * @param {Blob} blob - The blob object.
   * @returns {string} The temporary URL for the blob.
   */
  private createBlobURL(blob: Blob): string {
    return URL.createObjectURL(blob);
  }

  /**
   * Triggers the download of the database backup.
   *
   * @param {string} url - The temporary URL for the blob.
   */
  private downloadDatabaseBackup(url: string) {
    const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
    downloadAnchor.href = url;
    downloadAnchor.download = `${db.name}-backup.json`;
    downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo
  }

  public computedStartDownloadLinkClassNames() {
    return {
      invisible: this.downloadUrl,
    };
  }

  public computedDownloadLinkClassNames() {
    return {
      invisible: !this.downloadUrl,
    };
  }

  /**
   * Notifies the user that the download has started.
   */
  private notifyDownloadStarted() {
    this.$notify(
      {
        group: "alert",
        type: "success",
        title: "Download Started",
        text: "See your downloads directory for the backup. It is in the Dexie format.",
      },
      -1,
    );
  }

  /**
   * Handles errors during the database export process.
   *
   * @param {Error} error - The error object.
   */
  private handleExportError(error: unknown) {
    this.$notify(
      {
        group: "alert",
        type: "danger",
        title: "Export Error",
        text: "See console logs for more info.",
      },
      -1,
    );
    console.error("Export Error:", error);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  async uploadFile(event: any) {
    inputFileNameRef.value = event.target.files[0];
  }

  showContactImport() {
    return !!inputFileNameRef.value;
  }

  /**
   * Asynchronously imports the database from a downloadable JSON file.
   *
   * @throws Will notify the user if there is an export error.
   */
  async submitFile() {
    if (inputFileNameRef.value != null) {
      if (
        confirm(
          "This will replace all settings and contacts, so we recommend you first do the backup step above." +
            " Are you sure you want to import and replace all contacts and settings?",
        )
      ) {
        await db.delete();
        await Dexie.import(inputFileNameRef.value, {
          progressCallback: this.progressCallback,
        });
      }
    }
  }

  private progressCallback(progress: ImportProgress) {
    console.log(
      `Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
    );
    return true;
  }

  async checkLimits() {
    const identity = await this.getIdentity(this.activeDid);
    if (identity) {
      this.checkLimitsFor(identity);
    }
  }

  /**
   * Asynchronously checks rate limits for the given identity.
   *
   * Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
   */
  public async checkLimitsFor(identity: IIdentifier) {
    this.loadingLimits = true;
    this.limitsMessage = "";

    try {
      const resp = await this.fetchRateLimits(identity);
      if (resp.status === 200) {
        this.limits = resp.data;
        if (!this.isRegistered) {
          // the user is not known to be registered, but they are so let's record it
          try {
            await db.open();
            db.settings.update(MASTER_SETTINGS_KEY, {
              isRegistered: true,
            });
            this.isRegistered = true;
          } catch (err) {
            console.error("Got an error updating settings:", err);
            this.$notify(
              {
                group: "alert",
                type: "warning",
                title: "Update Error",
                text: "Unable to update your settings. Check claim limits again.",
              },
              -1,
            );
          }
        }
      }
    } catch (error) {
      this.handleRateLimitsError(error);
    }

    this.loadingLimits = false;
  }

  /**
   * Fetches rate limits from the server.
   *
   * @param {IIdentifier} identity - The identity object to check rate limits for.
   * @returns {Promise<AxiosResponse>} The Axios response object.
   */
  private async fetchRateLimits(identity: IIdentifier) {
    const url = `${this.apiServer}/api/report/rateLimits`;
    const headers = await this.getHeaders(identity);
    return await this.axios.get(url, { headers } as AxiosRequestConfig);
  }

  /**
   * Handles errors that occur while fetching rate limits.
   *
   * @param {AxiosError | Error} error - The error object.
   */
  private handleRateLimitsError(error: unknown) {
    if (error instanceof AxiosError) {
      const data = error.response?.data as ErrorResponse;
      this.limitsMessage =
        (data?.error?.message as string) || "Bad server response.";
      console.error(
        "Got bad response retrieving limits, which usually means user isn't registered. Server says:",
        this.limitsMessage,
      );
    } else if (
      error instanceof Error &&
      error.message ===
        "Attempted to load Give records with no identity available."
    ) {
      this.limitsMessage = "No identity.";
    } else {
      // Handle other unknown errors
    }
  }

  /**
   * Asynchronously switches the active account based on the provided account number.
   *
   * @param {number} accountNum - The account number to switch to. 0 means none.
   */
  public async switchAccount(accountNum: number) {
    await db.open(); // Assumes db needs to be open for both cases

    if (accountNum === 0) {
      this.switchToNoAccount();
    } else {
      await this.switchToAccountNumber(accountNum);
    }
  }

  /**
   * Switches to no active account and clears relevant properties.
   */
  private async switchToNoAccount() {
    await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: undefined });
    this.clearActiveAccountProperties();
  }

  /**
   * Clears properties related to the active account.
   */
  private clearActiveAccountProperties() {
    this.activeDid = "";
    this.derivationPath = "";
    this.publicHex = "";
    this.publicBase64 = "";
  }

  /**
   * Switches to an account based on its number in the list.
   *
   * @param {number} accountNum - The account number to switch to.
   */
  private async switchToAccountNumber(accountNum: number) {
    await accountsDB.open();
    const accounts = await accountsDB.accounts.toArray();
    const account = accounts[accountNum - 1];

    await db.open();
    await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });

    this.updateActiveAccountProperties(account);
  }

  /**
   * Updates properties related to the active account.
   *
   * @param {AccountType} account - The account object.
   */
  private updateActiveAccountProperties(account: IAccount) {
    this.activeDid = account.did;
    this.derivationPath = account.derivationPath;
    this.publicHex = account.publicKeyHex;
    this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
  }

  public showContactGivesClassNames() {
    return {
      "bg-slate-900": !this.showContactGives,
      "bg-green-600": this.showContactGives,
    };
  }

  async onClickSaveApiServer() {
    await db.open();
    db.settings.update(MASTER_SETTINGS_KEY, {
      apiServer: this.apiServerInput,
    });
    this.apiServer = this.apiServerInput;
  }

  async onClickSavePushServer() {
    await db.open();
    db.settings.update(MASTER_SETTINGS_KEY, {
      webPushServer: this.webPushServerInput,
    });
    this.webPushServer = this.webPushServerInput;
    this.$notify(
      {
        group: "alert",
        type: "warning",
        title: "Reload",
        text: "Now reload the app to get a new VAPID to use with this push server.",
      },
      -1,
    );
  }
}
</script>
