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

  <!-- CONTENT -->
  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
    <!-- Back -->
    <div class="text-lg text-center font-light relative px-7">
      <h1
        class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
        @click="$router.back()"
      >
        <fa icon="chevron-left" class="fa-fw"></fa>
      </h1>
    </div>

    <!-- 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-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] 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 share with others or take any action, you
        need an identifier.
      </p>
      <router-link
        :to="{ name: 'start' }"
        class="inline-block text-md uppercase bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
      >
        Create An Identifier
      </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>
        <div class="flex justify-center mt-4">
          <span v-if="profileImageUrl" class="flex justify-between">
            <a
              :href="profileImageUrl"
              target="_blank"
              class="text-blue-500 ml-4"
            >
              <img :src="profileImageUrl" class="h-24 rounded-xl" />
            </a>
            <fa
              icon="trash-can"
              @click="confirmDeleteImage"
              class="text-red-500 fa-fw ml-8 mt-10"
            />
          </span>
          <span v-else>
            <fa
              icon="camera"
              class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
              @click="openPhotoDialog"
            />
          </span>
          <GiftedPhotoDialog ref="photoDialog" />
        </div>
      </h2>
      <span v-else>
        <router-link
          :to="{ name: 'new-edit-account' }"
          class="block w-full text-center text-md bg-amber-200 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>

    <!-- 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 && !endorserLimits?.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-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
      >
        Share Your Info
      </router-link>
    </div>

    <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
      <!-- label -->
      <div class="mb-2 font-bold">Notifications</div>
      <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="pl-4 text-sm text-blue-500" to="/help-notifications">
        Troubleshoot your notification setup.
      </router-link>
    </div>

    <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
      <!-- label -->
      <div class="mb-2 font-bold">Location</div>
      <router-link
        :to="{ name: 'search-area' }"
        v-if="activeDid"
        class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6"
      >
        Set Search Area…
        <!-- If already set, change button label to "Change Search Area" -->
      </router-link>
    </div>

    <div
      v-if="activeDid"
      class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
    >
      <div class="mb-2 font-bold">Usage Limits</div>
      <!-- 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="!!endorserLimits?.nextWeekBeginDateTime">
        <p class="text-sm">
          You have done
          <b>{{ endorserLimits.doneClaimsThisWeek }} claims</b> out of
          <b>{{ endorserLimits.maxClaimsPerWeek }}</b> for this week. Your
          claims counter resets at
          <b class="whitespace-nowrap">{{
            readableDate(endorserLimits.nextWeekBeginDateTime)
          }}</b>
        </p>
        <p class="mt-3 text-sm">
          You have done
          <b>{{ endorserLimits.doneRegistrationsThisMonth }} registrations</b>
          out of <b>{{ endorserLimits.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">
            {{ readableDate(endorserLimits.nextMonthBeginDateTime) }}
          </b>
        </p>
        <p class="mt-3 text-sm" v-if="!!imageLimits">
          You have uploaded
          <b>{{ imageLimits?.doneImagesThisWeek }} images</b> out of
          <b>{{ imageLimits?.maxImagesPerWeek }}</b> for this week. Your image
          counter resets at
          <b class="whitespace-nowrap">{{
            readableDate(imageLimits?.nextWeekBeginDateTime)
          }}</b>
        </p>
      </div>
      <button
        class="block float-right w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mt-2"
        @click="checkLimits()"
      >
        Recheck Limits
      </button>
    </div>

    <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
      <div class="mb-2 font-bold">Data Export</div>
      <router-link
        :to="{ name: 'seed-backup' }"
        v-if="activeDid"
        class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
      >
        Backup Identifier Seed
      </router-link>

      <button
        v-bind:class="computedStartDownloadLinkClassNames()"
        class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
        @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-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
      >
        If no download happened yet, click again here to download now.
      </a>
    </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 -->
      <span class="text-slate-500 text-sm font-bold mb-2">
        Deep Identifier Details
      </span>
      <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>

      <!-- 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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
      >
        Switch Identifier
      </router-link>

      <label
        for="toggleShowAmounts"
        class="flex items-center justify-between cursor-pointer my-4"
        @click="toggleShowContactAmounts"
      >
        <!-- label -->
        <span class="text-slate-500 text-sm font-bold">Contacts Display</span>
        <span class="ml-2">Show amounts given</span>
        <!-- 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>
        <h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
        <div class="px-4 py-4">
          <input
            type="text"
            class="block w-full rounded border border-slate-400 px-4 py-2"
            v-model="apiServerInput"
          />
          <button
            v-if="apiServerInput != apiServer"
            class="w-full px-4 rounded bg-yellow-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 px-4 py-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 px-4 py-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>

      <h2 class="text-slate-500 text-sm font-bold mb-2">
        Notification Push Server
      </h2>
      <div class="px-3 py-4">
        <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="w-full px-4 rounded bg-yellow-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:
        {{ DEFAULT_PUSH_SERVER }}
      </span>

      <div class="mt-2">
        <span class="text-slate-500 text-sm font-bold">Image Server URL</span>
        &nbsp;
        <span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span>
      </div>

      <label
        for="toggleShowShortcutBvc"
        class="flex items-center justify-between cursor-pointer mt-4"
        @click="toggleShowShortcutBvc"
      >
        <!-- label -->
        <span class="text-slate-500 text-sm font-bold"
          >Show BVC Shortcut on Home Page</span
        >
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input type="checkbox" v-model="showShortcutBvc" 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="mt-4">
        <h2 class="text-slate-500 text-sm font-bold">
          Contacts & Settings Database
        </h2>

        <div class="ml-4 mt-2">
          Import
          <input type="file" @change="uploadFile" class="ml-2" />
          <div v-if="showContactImport()">
            <button
              class="block text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
              @click="submitFile()"
            >
              Import Settings & Contacts
              <br />
              (excluding Identifier Data)
            </button>
          </div>
        </div>
      </div>

      <div class="flex mt-4">
        <button>
          <router-link
            :to="{ name: 'statistics' }"
            class="block w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
          >
            See Global Animated History of Giving
          </router-link>
        </button>
      </div>
    </div>
  </section>
</template>

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

import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import {
  AppString,
  DEFAULT_IMAGE_API_SERVER,
  DEFAULT_PUSH_SERVER,
  NotificationIface,
} 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,
  EndorserRateLimits,
  ImageRateLimits,
} from "@/libs/endorserServer";
import { Buffer } from "buffer/";

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

const inputFileNameRef = ref<Blob>();

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

  AppConstants = AppString;
  DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
  DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER;

  activeDid = "";
  apiServer = "";
  apiServerInput = "";
  derivationPath = "";
  downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
  endorserLimits: EndorserRateLimits | null = null;
  givenName = "";
  imageLimits: ImageRateLimits | null = null;
  isRegistered = false;
  isSubscribed = false;
  notificationMaybeChanged = false;
  profileImageUrl: string | null = null;
  publicHex = "";
  publicBase64 = "";
  webPushServer = "";
  webPushServerInput = "";

  limitsMessage = "";
  loadingLimits = false;
  showContactGives = false;
  showDidCopy = false;
  showDerCopy = false;
  showB64Copy = false;
  showPubCopy = false;
  showAdvanced = false;
  showShortcutBvc = false;
  subscription: PushSubscription | null = null;
  warnIfProdServer = false;
  warnIfTestServer = false;

  /**
   * 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.showShortcutBvc = !!settings?.showShortcutBvc;
    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));
  }

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

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

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

  toggleShowShortcutBvc() {
    this.showShortcutBvc = !this.showShortcutBvc;
    this.updateShowShortcutBvc(this.showShortcutBvc);
  }

  readableDate(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 identifier available."
    ) {
      this.limitsMessage = "No identifier.";
    } else {
      console.error("Telling user to clear cache at page create because:", err);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Loading Account",
          text: "Clear your cache and start over (after data backup).",
        },
        -1,
      );
    }
  }

  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-amounts 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 prod-server-warning 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 test-server-warning setting update because:",
        err,
      );
    }
  }

  public async updateShowShortcutBvc(newSetting: boolean) {
    try {
      await db.open();
      db.settings.update(MASTER_SETTINGS_KEY, {
        showShortcutBvc: newSetting,
      });
    } catch (err) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Updating BVC Shortcut Setting",
          text: "The setting may not have saved. Try again, maybe after restarting the app.",
        },
        -1,
      );
      console.error(
        "Telling user to try again after BVC-shortcut 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);

      // Notify the user that the download has started
      this.notifyDownloadStarted();

      // Revoke the temporary URL -- after a pause to avoid DuckDuckGo download failure
      setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
    } 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 {
      hidden: this.downloadUrl,
    };
  }

  public computedDownloadLinkClassNames() {
    return {
      hidden: !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.`,
    );
    if (progress.done) {
      console.log(`Imported ${progress.completedTables} tables.`);
      this.$notify(
        {
          group: "alert",
          type: "success",
          title: "Import Complete",
          text: "",
        },
        5000,
      );
    }
    return true;
  }

  async checkLimits() {
    const identity = await this.getIdentity(this.activeDid);
    if (identity) {
      this.checkLimitsFor(identity);
    } else {
      this.limitsMessage =
        "You have no identifier, or your data has been corrupted.";
    }
  }

  /**
   * 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.fetchEndorserRateLimits(identity);
      if (resp.status === 200) {
        this.endorserLimits = resp.data;
        if (!this.isRegistered) {
          // the user was not known to be registered, but now they are (because we got no error) 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,
            );
          }
        }
        const imageResp = await this.fetchImageRateLimits(identity);
        if (imageResp.status === 200) {
          this.imageLimits = imageResp.data;
        }
      }
    } catch (error) {
      this.handleRateLimitsError(error);

      try {
        await db.open();
        db.settings.update(MASTER_SETTINGS_KEY, {
          isRegistered: false,
        });
        this.isRegistered = false;
      } catch (err) {
        console.error("Got an error marking user not registered:", err);
        // already set an error notification for the user
      }
    }

    this.loadingLimits = false;
  }

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

  /**
   * Fetches rate limits from the image server.
   *
   * @param {IIdentifier} identity - The identity object to check rate limits for.
   * @returns {Promise<AxiosResponse>} The Axios response object.
   */
  private async fetchImageRateLimits(identity: IIdentifier) {
    const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
    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.",
      );
      //console.error(error);
    } else {
      this.limitsMessage = "Got an error retrieving limits.";
      console.error("Got some error retrieving limits:", error);
    }
  }

  /**
   * 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,
    );
  }

  openPhotoDialog() {
    (this.$refs.photoDialog as GiftedPhotoDialog).open((imgUrl) => {
      this.profileImageUrl = imgUrl;
      console.log("Got image URL:", imgUrl);
    });
  }

  confirmDeleteImage() {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Are you sure you want to delete your profile picture?",
        text: "",
        onYes: this.deleteImage,
      },
      -1,
    );
  }

  async deleteImage() {
    if (!this.profileImageUrl) {
      return;
    }
    try {
      const identity = await this.getIdentity(this.activeDid);
      if (!identity) {
        throw Error("No identity found.");
      }
      const token = await accessToken(identity);
      const response = await this.axios.delete(
        DEFAULT_IMAGE_API_SERVER +
          "/image/" +
          encodeURIComponent(this.profileImageUrl),
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      );
      if (response.status === 204) {
        // don't bother with a notification
        // (either they'll simply continue or they're canceling and going back)
      } else {
        console.error("Non-success deleting image:", response);
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error",
            text: "There was a problem deleting the image.",
          },
          5000,
        );
        // keep the imageUrl in localStorage so the user can try again if they want
        return;
      }

      this.profileImageUrl = "";
    } catch (error) {
      console.error("Error deleting image:", error);
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      if ((error as any).response.status === 404) {
        console.log("The image was already deleted:", error);

        this.profileImageUrl = "";

        // it already doesn't exist so we won't say anything to the user
      } else {
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error",
            text: "There was an error deleting the image.",
          },
          5000,
        );
      }
    }
  }
}
</script>