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

  <!-- CONTENT -->
  <main
    id="Content"
    class="p-6 pb-24 max-w-3xl mx-auto"
    role="main"
    aria-label="Account Profile"
  >
    <!-- Heading -->
    <h1 id="ViewHeading" class="text-4xl text-center font-light">
      Your Identity
    </h1>

    <!-- ID notice -->
    <div
      v-if="!activeDid"
      id="noticeBeforeShare"
      class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
      role="alert"
      aria-live="polite"
    >
      <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 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-4 py-2 rounded-md"
      >
        Create An Identifier
      </router-link>
    </div>

    <!-- Identity Details -->
    <section
      id="sectionIdentityDetails"
      class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4"
      aria-labelledby="identityDetailsHeading"
    >
      <h2 id="identityDetailsHeading" class="sr-only">Identity Details</h2>
      <div v-if="givenName">
        <h2 class="text-xl font-semibold mb-2">
          <span class="whitespace-nowrap">
            <button
              class="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-1 mr-1 rounded-md"
              @click="handleQRCodeClick"
            >
              <font-awesome icon="qrcode" class="fa-fw text-xl" />
            </button>
          </span>
          {{ givenName }}
          <router-link :to="{ name: 'new-edit-account' }">
            <font-awesome
              icon="pen"
              class="text-xs text-blue-500 ml-2 mb-1"
            ></font-awesome>
          </router-link>
        </h2>
      </div>
      <span
        v-else
        class="block w-full text-center text-md bg-amber-200 border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
      >
        <button
          class="inline-block 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-4 py-2 rounded-md"
          @click="
            () =>
              ($refs.userNameDialog as UserNameDialog).open(
                (name) => (givenName = name),
              )
          "
        >
          Set Your Name
        </button>
        <p class="text-xs text-slate-500 mt-1">
          (Don't worry: this is not visible to anyone until you share it with
          them. It's not sent to any servers.)
        </p>
        <UserNameDialog ref="userNameDialog" />
      </span>
      <div class="flex justify-center mt-4">
        <span v-if="profileImageUrl" class="flex justify-between">
          <EntityIcon
            :icon-size="96"
            :profile-image-url="profileImageUrl"
            class="inline-block align-text-bottom border border-slate-300 rounded"
            role="button"
            aria-label="View profile image in large size"
            tabindex="0"
            @click="showLargeIdenticonUrl = profileImageUrl"
          />
          <font-awesome
            icon="trash-can"
            class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
            role="button"
            aria-label="Delete profile image"
            tabindex="0"
            @click="confirmDeleteImage"
          />
        </span>
        <div v-else class="text-center">
          <template v-if="isRegistered">
            <div
              class="inline-block text-md 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-4 py-2 rounded-md"
              @click="openImageDialog()"
            >
              <font-awesome icon="user" class="fa-fw" />
              <font-awesome icon="camera" class="fa-fw" />
            </div>
          </template>
          <!-- 
          If not registered, they don't need to see this at all. We show a prompt 
          to register below. 
          -->
        </div>
        <ImageMethodDialog
          ref="imageMethodDialog"
          :is-registered="isRegistered"
          default-camera-mode="user"
        />
      </div>
      <div class="mt-4">
        <div class="flex justify-center text-center text-sm leading-tight mb-1">
          People {{ profileImageUrl ? "without your image" : "" }} see this
          <br />
          (if you've let them see your activity):
        </div>
        <div class="flex justify-center">
          <EntityIcon
            :entity-id="activeDid"
            :icon-size="64"
            class="inline-block align-middle border border-slate-300 rounded-md mr-1"
            @click="showLargeIdenticonId = activeDid"
          />
        </div>
      </div>
      <div
        v-if="showLargeIdenticonId || showLargeIdenticonUrl"
        class="fixed z-[100] top-0 inset-x-0 w-full"
      >
        <div
          class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
        >
          <EntityIcon
            :entity-id="showLargeIdenticonId"
            :icon-size="512"
            :profile-image-url="showLargeIdenticonUrl"
            class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
            @click="
              showLargeIdenticonId = undefined;
              showLargeIdenticonUrl = undefined;
            "
          />
        </div>
      </div>

      <div
        class="text-sm text-slate-500 flex justify-start items-center mt-2 mb-1"
        data-testId="didWrapper"
        role="region"
        aria-label="Your Identifier"
      >
        <div class="font-bold">ID:&nbsp;</div>
        <code class="truncate" aria-label="Your DID">{{ activeDid }}</code>
        <button
          class="ml-2"
          aria-label="Copy DID to clipboard"
          @click="
            doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
          "
        >
          <font-awesome
            icon="copy"
            class="text-slate-400 fa-fw"
            aria-hidden="true"
          ></font-awesome>
        </button>
        <span v-show="showDidCopy" role="status" aria-live="polite"
          >Copied</span
        >
      </div>

      <div class="text-blue-500 text-sm font-bold">
        <router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }">
          Your Activity
        </router-link>
      </div>
    </section>

    <!-- 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="!isRegistered"
      id="noticeBeforeAnnounce"
      class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
      role="alert"
      aria-live="polite"
    >
      <p class="mb-2">
        Before you can publicly announce a new project or time commitment, a
        friend needs to register you.
      </p>
      <button
        class="inline-block text-md 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-4 py-2 rounded-md"
        @click="handleQRCodeClick"
      >
        Share Your Info
      </button>
    </div>

    <section
      v-if="isRegistered"
      id="sectionNotifications"
      class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
      aria-labelledby="notificationsHeading"
    >
      <h2 id="notificationsHeading" class="mb-2 font-bold">Notifications</h2>
      <div class="flex items-center justify-between">
        <div>
          Reminder Notification
          <button
            class="text-slate-400 fa-fw cursor-pointer"
            aria-label="Learn more about reminder notifications"
            @click.stop="showReminderNotificationInfo"
          >
            <font-awesome
              icon="question-circle"
              aria-hidden="true"
            ></font-awesome>
          </button>
        </div>
        <div
          class="relative ml-2 cursor-pointer"
          role="switch"
          :aria-checked="notifyingReminder"
          aria-label="Toggle reminder notifications"
          tabindex="0"
          @click="showReminderNotificationChoice()"
        >
          <!-- input -->
          <input v-model="notifyingReminder" type="checkbox" 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-if="notifyingReminder" class="w-full flex justify-between">
        <span class="ml-8 mr-8">Message: "{{ notifyingReminderMessage }}"</span>
        <span>{{ notifyingReminderTime.replace(" ", "&nbsp;") }}</span>
      </div>
      <div class="mt-2 flex items-center justify-between">
        <!-- label -->
        <div>
          New Activity Notification
          <font-awesome
            icon="question-circle"
            class="text-slate-400 fa-fw cursor-pointer"
            @click.stop="showNewActivityNotificationInfo"
          />
        </div>
        <!-- toggle -->
        <div
          class="relative ml-2 cursor-pointer"
          @click="showNewActivityNotificationChoice()"
        >
          <!-- input -->
          <input
            v-model="notifyingNewActivity"
            type="checkbox"
            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-if="notifyingNewActivityTime" class="w-full text-right">
        {{ notifyingNewActivityTime.replace(" ", "&nbsp;") }}
      </div>
      <div class="mt-2 text-center">
        <router-link class="text-sm text-blue-500" to="/help-notifications">
          Troubleshoot your notifications&hellip;
        </router-link>
      </div>
    </section>
    <PushNotificationPermission ref="pushNotificationPermission" />

    <section
      id="sectionSearchLocation"
      class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
      aria-labelledby="searchLocationHeading"
    >
      <h2 id="searchLocationHeading" class="mb-2 font-bold">
        Location for Searches
      </h2>
      <router-link
        :to="{ name: 'search-area' }"
        class="block w-full text-center 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-4 py-2 rounded-md"
      >
        {{ isSearchAreasSet ? "Change" : "Set" }} Search Area…
      </router-link>
    </section>

    <!-- User Profile -->
    <section
      v-if="isRegistered"
      class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
      aria-labelledby="userProfileHeading"
    >
      <h2 id="userProfileHeading" class="mb-2 font-bold">
        Public Profile
        <button
          class="text-slate-400 fa-fw cursor-pointer"
          aria-label="Learn more about public profile"
          @click="showProfileInfo"
        >
          <font-awesome icon="circle-info" aria-hidden="true"></font-awesome>
        </button>
      </h2>
      <textarea
        v-model="userProfileDesc"
        class="w-full h-32 p-2 border border-slate-300 rounded-md"
        placeholder="Write something about yourself for the public..."
        :readonly="loadingProfile || savingProfile"
        :class="{ 'bg-slate-100': loadingProfile || savingProfile }"
        aria-label="Public profile description"
        :aria-busy="loadingProfile || savingProfile"
      ></textarea>

      <div class="flex items-center mb-4" @click="toggleUserProfileLocation">
        <input
          v-model="includeUserProfileLocation"
          type="checkbox"
          class="mr-2"
        />
        <label for="includeUserProfileLocation">Include Location</label>
      </div>
      <div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
        <p class="text-sm mb-2 text-slate-500">
          The location you choose will be shared with the world until you remove
          this checkbox. For your security, choose a location nearby but not
          exactly at your true location, like at your town center.
        </p>

        <l-map
          ref="profileMap"
          class="!z-40 rounded-md"
          @click="
            (event: LeafletMouseEvent) => {
              userProfileLatitude = event.latlng.lat;
              userProfileLongitude = event.latlng.lng;
            }
          "
          @ready="onMapReady"
        >
          <l-tile-layer
            url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
            layer-type="base"
            name="OpenStreetMap"
          />
          <l-marker
            v-if="userProfileLatitude && userProfileLongitude"
            :lat-lng="[userProfileLatitude, userProfileLongitude]"
            @click="confirmEraseLatLong()"
          />
        </l-map>
      </div>
      <div v-if="!loadingProfile && !savingProfile">
        <div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4">
          <button
            class="px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
            :disabled="loadingProfile || savingProfile"
            :class="{
              'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
            }"
            @click="saveProfile"
          >
            Save Profile
          </button>
          <button
            class="px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
            :disabled="loadingProfile || savingProfile"
            :class="{
              'opacity-50 cursor-not-allowed':
                loadingProfile ||
                savingProfile ||
                (!userProfileDesc && !includeUserProfileLocation),
            }"
            @click="confirmDeleteProfile"
          >
            Delete Profile
          </button>
        </div>
      </div>
      <div v-else-if="loadingProfile">Loading...</div>
      <div v-else>Saving...</div>
    </section>

    <section
      v-if="activeDid"
      id="sectionUsageLimits"
      class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
      aria-labelledby="usageLimitsHeading"
    >
      <h2 id="usageLimitsHeading" class="mb-2 font-bold">Usage Limits</h2>
      <!-- show spinner if loading limits -->
      <div
        v-if="loadingLimits"
        class="text-center"
        role="status"
        aria-live="polite"
      >
        Checking&hellip;
        <font-awesome
          icon="spinner"
          class="fa-spin"
          aria-hidden="true"
        ></font-awesome>
      </div>
      <div class="mb-4 text-center">
        {{ limitsMessage }}
      </div>
      <div v-if="endorserLimits">
        <p class="text-sm">
          You have done
          <b
            >{{ endorserLimits?.doneClaimsThisWeek ?? "?" }} claim{{
              endorserLimits?.doneClaimsThisWeek === 1 ? "" : "s"
            }}</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 ?? "?"
            }}
            registration{{
              endorserLimits?.doneRegistrationsThisMonth === 1 ? "" : "s"
            }}</b
          >
          out of
          <b>{{ endorserLimits?.maxRegistrationsPerMonth ?? "?" }}</b> for this
          this month.
          <i>(You cannot register anyone on your first day.)</i>
          Your registration counter resets at
          <b class="whitespace-nowrap">
            {{ readableDate(endorserLimits?.nextMonthBeginDateTime) }}
          </b>
        </p>
        <p class="mt-3 text-sm">
          You have uploaded
          <b
            >{{ imageLimits?.doneImagesThisWeek ?? "?" }} image{{
              imageLimits?.doneImagesThisWeek === 1 ? "" : "s"
            }}</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 w-full text-center text-md 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-4"
        @click="checkLimits()"
      >
        Recheck Limits
      </button>
    </section>

    <DataExportSection :active-did="activeDid" />

    <!-- id used by puppeteer test script -->
    <h3
      id="advanced"
      class="text-blue-500 text-sm font-semibold mb-3 cursor-pointer"
      @click="showAdvanced = !showAdvanced"
    >
      {{ showAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings" }}
    </h3>
    <section
      v-if="showAdvanced || showGeneralAdvanced"
      id="sectionAdvanced"
      aria-labelledby="advancedHeading"
    >
      <h2 id="advancedHeading" class="sr-only">Advanced Settings</h2>
      <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">
        Identifier Details
      </span>
      <div
        id="sectionDeepIdentifier"
        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
            class="ml-2"
            @click="
              doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
            "
          >
            <font-awesome
              icon="copy"
              class="text-slate-400 fa-fw"
            ></font-awesome>
          </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
            class="ml-2"
            @click="
              doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
            "
          >
            <font-awesome
              icon="copy"
              class="text-slate-400 fa-fw"
            ></font-awesome>
          </button>
          <span v-show="showPubCopy">Copied</span>
        </div>

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

      <div id="sectionImportContactsSettings" class="mt-4">
        <h2 class="text-slate-500 text-sm font-bold">Import Contacts</h2>

        <div class="ml-4 mt-2">
          <input type="file" class="ml-2" @change="uploadImportFile" />
          <transition
            enter-active-class="transform ease-out duration-300 transition"
            enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
            enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
            leave-active-class="transition ease-in duration-500"
            leave-from-class="opacity-100"
            leave-to-class="opacity-0"
          >
            <div v-if="showContactImport()" class="mt-4">
              <!-- Bulk import has an error
              <div class="flex justify-center">
                <button
                  class="block text-center text-md 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="confirmSubmitImportFile()"
                >
                  Overwrite Settings & Contacts
                  <br />
                  (which doesn't include Identifier Data)
                </button>
              </div>
              -->
              <div class="flex justify-center">
                <button
                  class="block text-center text-md 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="checkContactImports()"
                >
                  Import Contacts
                </button>
              </div>
            </div>
          </transition>
        </div>
      </div>

      <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 hours given & received</span>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input
            v-model="showContactGives"
            type="checkbox"
            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 id="sectionClaimServer">
        <h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
        <div
          class="px-4 py-4"
          role="group"
          aria-labelledby="claimServerHeading"
        >
          <h3 id="claimServerHeading" class="sr-only">
            Claim Server Configuration
          </h3>
          <label for="apiServerInput" class="sr-only">API Server URL</label>
          <input
            id="apiServerInput"
            v-model="apiServerInput"
            type="text"
            class="block w-full rounded border border-slate-400 px-4 py-2"
            aria-describedby="apiServerDescription"
            placeholder="Enter API server URL"
          />
          <div id="apiServerDescription" class="sr-only" role="tooltip">
            Enter the URL for the claim server. You can use the buttons below to
            quickly set common server URLs.
          </div>
          <button
            v-if="apiServerInput != apiServer"
            class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
            aria-label="Save API server URL"
            @click="onClickSaveApiServer()"
          >
            <font-awesome
              icon="floppy-disk"
              class="fa-fw"
              color="white"
              aria-hidden="true"
            ></font-awesome>
          </button>
          <div class="mt-2" role="group" aria-label="Quick server selection">
            <button
              class="px-3 rounded bg-slate-200 border border-slate-400"
              aria-label="Use production server URL"
              @click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
            >
              Use Prod
            </button>
            <button
              class="px-3 rounded bg-slate-200 border border-slate-400"
              aria-label="Use test server URL"
              @click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
            >
              Use Test
            </button>
            <button
              class="px-3 rounded bg-slate-200 border border-slate-400"
              aria-label="Use local server URL"
              @click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
            >
              Use Local
            </button>
          </div>
        </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 v-model="warnIfProdServer" type="checkbox" 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 v-model="warnIfTestServer" type="checkbox" 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
          v-model="webPushServerInput"
          type="text"
          class="block w-full rounded border border-slate-400 px-3 py-2"
        />
        <button
          v-if="webPushServerInput != webPushServer"
          class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
          @click="onClickSavePushServer()"
        >
          <font-awesome
            icon="floppy-disk"
            class="fa-fw"
            color="white"
          ></font-awesome>
        </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 v-if="!webPushServerInput" class="px-4 text-sm">
        When that setting is blank, this app will use the default web push
        server URL:
        {{ DEFAULT_PUSH_SERVER }}
      </span>

      <h2 class="text-slate-500 text-sm font-bold mb-2">Partner Server URL</h2>
      <div class="px-3 py-4">
        <input
          v-model="partnerApiServerInput"
          type="text"
          class="block w-full rounded border border-slate-400 px-3 py-2"
        />
        <button
          v-if="partnerApiServerInput != partnerApiServer"
          class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
          @click="onClickSavePartnerServer()"
        >
          <font-awesome
            icon="floppy-disk"
            class="fa-fw"
            color="white"
          ></font-awesome>
        </button>
        <button
          class="px-3 rounded bg-slate-200 border border-slate-400"
          @click="partnerApiServerInput = AppConstants.PROD_PARTNER_API_SERVER"
        >
          Use Prod
        </button>
        <button
          class="px-3 rounded bg-slate-200 border border-slate-400"
          @click="partnerApiServerInput = AppConstants.TEST_PARTNER_API_SERVER"
        >
          Use Test
        </button>
        <button
          class="px-3 rounded bg-slate-200 border border-slate-400"
          @click="partnerApiServerInput = AppConstants.LOCAL_PARTNER_API_SERVER"
        >
          Use Local
        </button>
      </div>
      <span v-if="!partnerApiServerInput" class="px-4 text-sm">
        When that setting is blank, this app will use the default partner server
        URL:
        {{ DEFAULT_PARTNER_API_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="toggleHideRegisterPromptOnNewContact"
        class="flex items-center justify-between cursor-pointer mt-4"
        @click="toggleHideRegisterPromptOnNewContact()"
      >
        <!-- label -->
        <span class="text-slate-500 text-sm font-bold">
          Hide Register Prompt on New Contact
        </span>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input
            v-model="hideRegisterPromptOnNewContact"
            type="checkbox"
            class="sr-only"
          />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full" />
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          />
        </div>
      </label>

      <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 v-model="showShortcutBvc" type="checkbox" class="sr-only" />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full" />
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          />
        </div>
      </label>

      <div id="sectionPasskeyExpiration" class="flex mt-4 justify-between">
        <span>
          <span class="text-slate-500 text-sm font-bold">
            Passkey Expiration Minutes
          </span>
          <br />
          <span class="text-sm ml-2">
            {{ passkeyExpirationDescription }}
          </span>
        </span>
        <div class="relative ml-2">
          <input
            v-model="passkeyExpirationMinutes"
            type="number"
            class="border border-slate-400 rounded px-2 py-2 text-center w-20"
            @change="updatePasskeyExpiration"
          />
        </div>
      </div>

      <label
        for="toggleShowGeneralAdvanced"
        class="flex items-center justify-between cursor-pointer mt-4"
        @click="toggleShowGeneralAdvanced"
      >
        <!-- label -->
        <span class="text-slate-500 text-sm font-bold">
          Show All General Advanced Functions
        </span>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input
            v-model="showGeneralAdvanced"
            type="checkbox"
            class="sr-only"
          />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full" />
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          />
        </div>
      </label>

      <router-link
        :to="{ name: 'logs' }"
        class="block w-fit text-center text-md 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"
      >
        Logs
      </router-link>
      <router-link
        :to="{ name: 'test' }"
        class="block w-fit text-center text-md 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"
      >
        Test Page
      </router-link>

      <div class="flex mt-2">
        <button>
          <router-link
            :to="{ name: 'statistics' }"
            class="block w-fit text-center text-md 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"
          >
            See Global Animated History of Giving
          </router-link>
        </button>
      </div>
    </section>
  </main>
</template>

<script lang="ts">
import "leaflet/dist/leaflet.css";

import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie";
import "dexie-export-import";
// @ts-expect-error - they aren't exporting it but it's there
import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { Capacitor } from "@capacitor/core";

import EntityIcon from "../components/EntityIcon.vue";
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import DataExportSection from "../components/DataExportSection.vue";
import {
  AppString,
  DEFAULT_IMAGE_API_SERVER,
  DEFAULT_PARTNER_API_SERVER,
  DEFAULT_PUSH_SERVER,
  IMAGE_TYPE_PROFILE,
  NotificationIface,
  USE_DEXIE_DB,
} from "../constants/app";
import {
  db,
  logConsoleAndDb,
  retrieveSettingsForActiveAccount,
  updateAccountSettings,
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import {
  DEFAULT_PASSKEY_EXPIRATION_MINUTES,
  MASTER_SETTINGS_KEY,
} from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import {
  clearPasskeyToken,
  EndorserRateLimits,
  ErrorResponse,
  errorStringForLog,
  fetchEndorserRateLimits,
  fetchImageRateLimits,
  getHeaders,
  ImageRateLimits,
  tokenExpiryTimeDescription,
} from "../libs/endorserServer";
import {
  DAILY_CHECK_TITLE,
  DIRECT_PUSH_TITLE,
  retrieveAccountMetadata,
} from "../libs/util";
import { UserProfile } from "@/libs/partnerServer";
import { logger } from "../utils/logger";

const inputImportFileNameRef = ref<Blob>();

@Component({
  components: {
    EntityIcon,
    ImageMethodDialog,
    LeafletMouseEvent,
    LMap,
    LMarker,
    LTileLayer,
    PushNotificationPermission,
    QuickNav,
    TopMessage,
    UserNameDialog,
    DataExportSection,
  },
})
export default class AccountViewView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  $route!: RouteLocationNormalizedLoaded;
  $router!: Router;

  AppConstants = AppString;
  DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
  DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER;
  DEFAULT_PARTNER_API_SERVER = DEFAULT_PARTNER_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 = "";
  hideRegisterPromptOnNewContact = false;
  imageLimits: ImageRateLimits | null = null;
  includeUserProfileLocation = false;
  isRegistered = false;
  isSearchAreasSet = false;
  limitsMessage = "";
  loadingLimits = false;
  loadingProfile = true;
  notifyingNewActivity = false;
  notifyingNewActivityTime = "";
  notifyingReminder = false;
  notifyingReminderMessage = "";
  notifyingReminderTime = "";
  partnerApiServer = DEFAULT_PARTNER_API_SERVER;
  partnerApiServerInput = DEFAULT_PARTNER_API_SERVER;
  passkeyExpirationDescription = "";
  passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
  previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
  profileImageUrl?: string;
  publicHex = "";
  publicBase64 = "";
  savingProfile = false;
  showAdvanced = false;
  showB64Copy = false;
  showContactGives = false;
  showDidCopy = false;
  showDerCopy = false;
  showGeneralAdvanced = false;
  showLargeIdenticonId?: string;
  showLargeIdenticonUrl?: string;
  showPubCopy = false;
  showShortcutBvc = false;
  subscription: PushSubscription | null = null;
  warnIfProdServer = false;
  warnIfTestServer = false;
  webPushServer = DEFAULT_PUSH_SERVER;
  webPushServerInput = DEFAULT_PUSH_SERVER;
  userProfileDesc = "";
  userProfileLatitude = 0;
  userProfileLongitude = 0;
  zoom = 2;

  /**
   * Async function executed when the component is mounted.
   * 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 mounted() {
    try {
      // Initialize component state with values from the database or defaults
      await this.initializeState();
      await this.processIdentity();

      // Load the user profile
      if (this.isRegistered) {
        try {
          const headers = await getHeaders(this.activeDid);
          const response = await this.axios.get(
            this.partnerApiServer +
              "/api/partner/userProfileForIssuer/" +
              this.activeDid,
            { headers },
          );
          if (response.status === 200) {
            this.userProfileDesc = response.data.data.description || "";
            this.userProfileLatitude = response.data.data.locLat || 0;
            this.userProfileLongitude = response.data.data.locLon || 0;
            if (this.userProfileLatitude && this.userProfileLongitude) {
              this.includeUserProfileLocation = true;
            }
          } else {
            // won't get here because axios throws an error instead
            throw Error("Unable to load profile.");
          }
        } catch (error) {
          if (error.status === 404) {
            // this is ok: the profile is not yet created
          } else {
            databaseUtil.logConsoleAndDb(
              "Error loading profile: " + errorStringForLog(error),
            );
            if (USE_DEXIE_DB) {
              logConsoleAndDb(
                "Error loading profile: " + errorStringForLog(error),
              );
            }
            this.$notify(
              {
                group: "alert",
                type: "danger",
                title: "Error Loading Profile",
                text: "Your server profile is not available.",
              },
              5000,
            );
          }
        }
      }
    } catch (error) {
      // this can happen when running automated tests in dev mode because notifications don't work
      logger.error(
        "Telling user to clear cache at page create because:",
        error,
      );
      // this sometimes gives different information on the error
      logger.error(
        "To repeat with concatenated error: telling user to clear cache at page create because: " +
          error,
      );
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Loading Profile",
          text: "See the Help page about errors with your personal data.",
        },
        5000,
      );
    } finally {
      this.loadingProfile = false;
    }

    try {
      /**
       * Beware! I've seen where this "ready" never resolves.
       */
      const registration = await navigator.serviceWorker?.ready;
      this.subscription = await registration.pushManager.getSubscription();
      if (!this.subscription) {
        if (this.notifyingNewActivity || this.notifyingReminder) {
          // the app thought there was a subscription but there isn't, so fix the settings
          this.turnOffNotifyingFlags();
        }
      }
      /**
       * Beware! I've seen where we never get to this point because "ready" never resolves.
       */
    } catch (error) {
      this.$notify(
        {
          group: "alert",
          type: "warning",
          title: "Cannot Set Notifications",
          text: "This browser does not support notifications. Use Chrome, or install this to the home screen, or try other suggestions on the 'Troubleshoot your notifications' page.",
        },
        7000,
      );
    }
    this.passkeyExpirationDescription = tokenExpiryTimeDescription();
  }

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

  /**
   * Initializes component state with values from the database or defaults.
   */
  async initializeState() {
    let settings = await databaseUtil.retrieveSettingsForActiveAccount();
    if (USE_DEXIE_DB) {
      await db.open();
      settings = await retrieveSettingsForActiveAccount();
    }

    this.activeDid = settings.activeDid || "";
    this.apiServer = settings.apiServer || "";
    this.apiServerInput = settings.apiServer || "";
    this.givenName =
      (settings?.firstName || "") +
      (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
    this.hideRegisterPromptOnNewContact =
      !!settings.hideRegisterPromptOnNewContact;
    this.isRegistered = !!settings?.isRegistered;
    this.isSearchAreasSet = !!settings.searchBoxes;
    this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
    this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
    this.notifyingReminder = !!settings.notifyingReminderTime;
    this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
    this.notifyingReminderTime = settings.notifyingReminderTime || "";
    this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
    this.partnerApiServerInput =
      settings.partnerApiServer || this.partnerApiServerInput;
    this.profileImageUrl = settings.profileImageUrl;
    this.showContactGives = !!settings.showContactGivesInline;
    this.passkeyExpirationMinutes =
      settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES;
    this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
    this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
    this.showShortcutBvc = !!settings.showShortcutBvc;
    this.warnIfProdServer = !!settings.warnIfProdServer;
    this.warnIfTestServer = !!settings.warnIfTestServer;
    this.webPushServer = settings.webPushServer || this.webPushServer;
    this.webPushServerInput = settings.webPushServer || this.webPushServerInput;
  }

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

  async toggleShowContactAmounts() {
    this.showContactGives = !this.showContactGives;
    await databaseUtil.updateDefaultSettings({
      showContactGivesInline: this.showContactGives,
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        showContactGivesInline: this.showContactGives,
      });
    }
  }

  async toggleShowGeneralAdvanced() {
    this.showGeneralAdvanced = !this.showGeneralAdvanced;
    await databaseUtil.updateDefaultSettings({
      showGeneralAdvanced: this.showGeneralAdvanced,
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        showGeneralAdvanced: this.showGeneralAdvanced,
      });
    }
  }

  async toggleProdWarning() {
    this.warnIfProdServer = !this.warnIfProdServer;
    await databaseUtil.updateDefaultSettings({
      warnIfProdServer: this.warnIfProdServer,
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        warnIfProdServer: this.warnIfProdServer,
      });
    }
  }

  async toggleTestWarning() {
    this.warnIfTestServer = !this.warnIfTestServer;
    await databaseUtil.updateDefaultSettings({
      warnIfTestServer: this.warnIfTestServer,
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        warnIfTestServer: this.warnIfTestServer,
      });
    }
  }

  async toggleShowShortcutBvc() {
    this.showShortcutBvc = !this.showShortcutBvc;
    await databaseUtil.updateDefaultSettings({
      showShortcutBvc: this.showShortcutBvc,
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        showShortcutBvc: this.showShortcutBvc,
      });
    }
  }

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

  /**
   * Processes the identity and updates the component's state.
   */
  async processIdentity() {
    const account = await retrieveAccountMetadata(this.activeDid);
    if (account?.identity) {
      const identity = JSON.parse(account.identity as string) as IIdentifier;
      this.publicHex = identity.keys[0].publicKeyHex;
      this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
      this.derivationPath = identity.keys[0].meta?.derivationPath as string;
      await this.checkLimits();
    } else if (account?.publicKeyHex) {
      // use the backup values in the top level of the account object
      this.publicHex = account.publicKeyHex as string;
      this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
      this.derivationPath = account.derivationPath as string;
      await this.checkLimits();
    }
  }

  async showNewActivityNotificationInfo() {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "New Activity Notification",
        text: `
          This will only notify you when there is new relevant activity for you personally.
          Note that it runs on your device and many factors may affect delivery,
          so if you want a reliable but simple daily notification then choose a 'Reminder'.
          Do you want more details?
        `,
        onYes: async () => {
          await (this.$router as Router).push({
            name: "help-notification-types",
          });
        },
        yesText: "tell me more.",
      },
      -1,
    );
  }

  async showNewActivityNotificationChoice() {
    if (!this.notifyingNewActivity) {
      (
        this.$refs.pushNotificationPermission as PushNotificationPermission
      ).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
        if (success) {
          await databaseUtil.updateDefaultSettings({
            notifyingNewActivityTime: timeText,
          });
          if (USE_DEXIE_DB) {
            await db.settings.update(MASTER_SETTINGS_KEY, {
              notifyingNewActivityTime: timeText,
            });
          }
          this.notifyingNewActivity = true;
          this.notifyingNewActivityTime = timeText;
        }
      });
    } else {
      this.$notify(
        {
          group: "modal",
          type: "notification-off",
          title: DAILY_CHECK_TITLE, // repurposed to indicate the type of notification
          text: "", // unused, only here to satisfy type check
          callback: async (success) => {
            if (success) {
              await databaseUtil.updateDefaultSettings({
                notifyingNewActivityTime: "",
              });
              if (USE_DEXIE_DB) {
                await db.settings.update(MASTER_SETTINGS_KEY, {
                  notifyingNewActivityTime: "",
                });
              }
              this.notifyingNewActivity = false;
              this.notifyingNewActivityTime = "";
            }
          },
        },
        -1,
      );
    }
  }

  async showReminderNotificationInfo() {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Reminder Notification",
        text: `
          This will notify you at a specific time each day.
          Note that it does not give you personalized notifications,
          so if you want less reliable but personalized notification then choose a 'New Activity' Notification.
          Do you want more details?
        `,
        onYes: async () => {
          await (this.$router as Router).push({
            name: "help-notification-types",
          });
        },
        yesText: "tell me more.",
      },
      -1,
    );
  }

  async showReminderNotificationChoice() {
    if (!this.notifyingReminder) {
      (
        this.$refs.pushNotificationPermission as PushNotificationPermission
      ).open(
        DIRECT_PUSH_TITLE,
        async (success: boolean, timeText: string, message?: string) => {
          if (success) {
            await databaseUtil.updateDefaultSettings({
              notifyingReminderMessage: message,
              notifyingReminderTime: timeText,
            });
            if (USE_DEXIE_DB) {
              await db.settings.update(MASTER_SETTINGS_KEY, {
                notifyingReminderMessage: message,
                notifyingReminderTime: timeText,
              });
            }
            this.notifyingReminder = true;
            this.notifyingReminderMessage = message || "";
            this.notifyingReminderTime = timeText;
          }
        },
      );
    } else {
      this.$notify(
        {
          group: "modal",
          type: "notification-off",
          title: DIRECT_PUSH_TITLE, // repurposed to indicate the type of notification
          text: "", // unused, only here to satisfy type check
          callback: async (success) => {
            if (success) {
              await databaseUtil.updateDefaultSettings({
                notifyingReminderMessage: "",
                notifyingReminderTime: "",
              });
              if (USE_DEXIE_DB) {
                await db.settings.update(MASTER_SETTINGS_KEY, {
                  notifyingReminderMessage: "",
                  notifyingReminderTime: "",
                });
              }
              this.notifyingReminder = false;
              this.notifyingReminderMessage = "";
              this.notifyingReminderTime = "";
            }
          },
        },
        -1,
      );
    }
  }

  public async toggleHideRegisterPromptOnNewContact() {
    const newSetting = !this.hideRegisterPromptOnNewContact;
    await databaseUtil.updateDefaultSettings({
      hideRegisterPromptOnNewContact: newSetting,
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        hideRegisterPromptOnNewContact: newSetting,
      });
    }
    this.hideRegisterPromptOnNewContact = newSetting;
  }

  public async updatePasskeyExpiration() {
    await databaseUtil.updateDefaultSettings({
      passkeyExpirationMinutes: this.passkeyExpirationMinutes,
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        passkeyExpirationMinutes: this.passkeyExpirationMinutes,
      });
    }
    clearPasskeyToken();
    this.passkeyExpirationDescription = tokenExpiryTimeDescription();
  }

  public async turnOffNotifyingFlags() {
    // should tell the push server as well
    await databaseUtil.updateDefaultSettings({
      notifyingNewActivityTime: "",
      notifyingReminderMessage: "",
      notifyingReminderTime: "",
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        notifyingNewActivityTime: "",
        notifyingReminderMessage: "",
        notifyingReminderTime: "",
      });
    }
    this.notifyingNewActivity = false;
    this.notifyingNewActivityTime = "";
    this.notifyingReminder = false;
    this.notifyingReminderMessage = "";
    this.notifyingReminderTime = "";
  }

  /**
   * 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> {
    if (USE_DEXIE_DB) {
      return await db.export({ prettyJson: true });
    }
    throw new Error("Not implemented");
  }

  /**
   * 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 = `${AppString.APP_NAME_NO_SPACES}-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) {
    logger.error("Export Error:", error);
    this.$notify(
      {
        group: "alert",
        type: "danger",
        title: "Export Error",
        text: "There was an error exporting the data.",
      },
      3000,
    );
  }

  async uploadImportFile(event: Event) {
    inputImportFileNameRef.value = (event.target as EventTarget).files[0];
  }

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

  confirmSubmitImportFile() {
    if (inputImportFileNameRef.value != null) {
      this.$notify(
        {
          group: "modal",
          type: "confirm",
          title: "Replace All",
          text:
            "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?",
          onYes: this.submitImportFile,
        },
        -1,
      );
    }
  }

  /**
   * Asynchronously imports the database from a downloadable JSON file.
   *
   * @throws Will notify the user if there is an export error.
   */
  async submitImportFile() {
    if (inputImportFileNameRef.value != null) {
      if (USE_DEXIE_DB) {
        await db
          .delete()
          .then(async () => {
            // BulkError: settings.bulkAdd(): 1 of 21 operations failed. Errors: ConstraintError: Key already exists in the object store.
            await Dexie.import(inputImportFileNameRef.value as Blob, {
              progressCallback: this.progressCallback,
            });
          })
          .catch((error) => {
            logger.error("Error importing file:", error);
            this.$notify(
              {
                group: "alert",
                type: "danger",
                title: "Error Importing",
                text: "There was an error in the import. Your identities and contacts may have been affected, so you may have to restore your identifier and use the contact import method.",
              },
              -1,
            );
          });
      } else {
        throw new Error("Not implemented");
      }
    }
  }

  async checkContactImports() {
    const reader = new FileReader();
    reader.onload = (event) => {
      const fileContent: string = (event.target?.result as string) || "{}";
      try {
        const contents = JSON.parse(fileContent);
        const contactTableRows: Array<Contact> = (
          contents.data?.data as [{ tableName: string; rows: Array<Contact> }]
        )?.find((table) => table.tableName === "contacts")
          ?.rows as Array<Contact>;
        const contactRows = contactTableRows.map(
          // @ts-expect-error for omitting this field that is found in the Dexie format
          (contact) => R.omit(["$types"], contact) as Contact,
        );
        (this.$router as Router).push({
          name: "contact-import",
          query: { contacts: JSON.stringify(contactRows) },
        });
      } catch (error) {
        logger.error("Error checking contact imports:", error);
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error Importing",
            text: "There was an error reading that Dexie file.",
          },
          3000,
        );
      }
    };
    reader.readAsText(inputImportFileNameRef.value as Blob);
  }

  private progressCallback(progress: ImportProgress) {
    logger.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() {
    if (this.activeDid) {
      this.checkLimitsFor(this.activeDid);
    } else {
      this.limitsMessage =
        "You have no identifier, or your data has been corrupted.";
    }
  }

  /**
   * Use "checkLimits" instead.
   *
   * Asynchronously checks rate limits for the given identity.
   *
   * Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
   */
  private async checkLimitsFor(did: string) {
    this.loadingLimits = true;
    this.limitsMessage = "";

    try {
      const resp = await fetchEndorserRateLimits(
        this.apiServer,
        this.axios,
        did,
      );
      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 databaseUtil.updateDidSpecificSettings(did, {
              isRegistered: true,
            });
            if (USE_DEXIE_DB) {
              await db.open();
              await db.settings.update(MASTER_SETTINGS_KEY, {
                isRegistered: true,
              });
            }
            this.isRegistered = true;
          } catch (err) {
            logger.error("Got an error updating settings:", err);
            this.$notify(
              {
                group: "alert",
                type: "danger",
                title: "Update Error",
                text: "Unable to update your settings. Check claim limits again.",
              },
              5000,
            );
          }
        }
        try {
          const imageResp = await fetchImageRateLimits(this.axios, did);
          if (imageResp.status === 200) {
            this.imageLimits = imageResp.data;
          } else {
            this.limitsMessage = "You don't have access to upload images.";
          }
        } catch {
          this.limitsMessage = "You cannot upload images.";
        }
      }
    } catch (error) {
      this.handleRateLimitsError(error);
    }

    this.loadingLimits = false;
  }

  /**
   * Handles errors that occur while fetching rate limits.
   *
   * @param {AxiosError | Error} error - The error object.
   */
  private handleRateLimitsError(error: unknown) {
    if (error instanceof AxiosError) {
      if (error.status == 400 || error.status == 404) {
        // no worries: they probably just aren't registered and don't have any limits
        logger.log(
          "Got 400 or 404 response retrieving limits which probably means they're not registered:",
          error,
        );
        this.limitsMessage = "No limits were found, so no actions are allowed.";
      } else {
        const data = error.response?.data as ErrorResponse;
        this.limitsMessage =
          (data?.error?.message as string) || "Bad server response.";
        logger.error("Got bad response retrieving limits:", error);
      }
    } else {
      this.limitsMessage = "Got an error retrieving limits.";
      logger.error("Got some error retrieving limits:", error);
    }
  }

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

  async onClickSavePartnerServer() {
    await databaseUtil.updateDefaultSettings({
      partnerApiServer: this.partnerApiServerInput,
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        partnerApiServer: this.partnerApiServerInput,
      });
    }
    this.partnerApiServer = this.partnerApiServerInput;
  }

  async onClickSavePushServer() {
    await databaseUtil.updateDefaultSettings({
      webPushServer: this.webPushServerInput,
    });
    if (USE_DEXIE_DB) {
      await db.open();
      await 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.",
      },
      5000,
    );
  }

  openImageDialog() {
    (this.$refs.imageMethodDialog as ImageMethodDialog).open(
      async (imgUrl) => {
        await databaseUtil.updateDefaultSettings({
          profileImageUrl: imgUrl,
        });
        if (USE_DEXIE_DB) {
          await db.open();
          await db.settings.update(MASTER_SETTINGS_KEY, {
            profileImageUrl: imgUrl,
          });
        }
        this.profileImageUrl = imgUrl;
        //console.log("Got image URL:", imgUrl);
      },
      IMAGE_TYPE_PROFILE,
      true,
    );
  }

  confirmDeleteImage() {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title:
          "Note that anyone with you already as a contact will no longer see a picture, and you will have to reshare your data with them if you save a new picture. Are you sure you want to delete your profile picture?",
        text: "",
        onYes: this.deleteImage,
      },
      -1,
    );
  }

  async deleteImage() {
    if (!this.profileImageUrl) {
      return;
    }
    try {
      const headers = await getHeaders(this.activeDid);
      this.passkeyExpirationDescription = tokenExpiryTimeDescription();
      if (
        window.location.hostname === "localhost" &&
        !DEFAULT_IMAGE_API_SERVER.includes("localhost")
      ) {
        logger.log(
          "Using shared image API server, so only users on that server can play with images.",
        );
      }
      const response = await this.axios.delete(
        DEFAULT_IMAGE_API_SERVER +
          "/image/" +
          encodeURIComponent(this.profileImageUrl),
        { headers },
      );
      if (response.status === 204) {
        // don't bother with a notification
        // (either they'll simply continue or they're canceling and going back)
      } else {
        logger.error("Non-success deleting image:", response);
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error",
            text: "There was a problem deleting the image. Contact support if you want it removed from the servers.",
          },
          5000,
        );
        // keep the imageUrl in localStorage so the user can try again if they want
      }

      await databaseUtil.updateDefaultSettings({
        profileImageUrl: undefined,
      });
      if (USE_DEXIE_DB) {
        await db.open();
        await db.settings.update(MASTER_SETTINGS_KEY, {
          profileImageUrl: undefined,
        });
      }

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

        await databaseUtil.updateDidSpecificSettings(this.activeDid, {
          profileImageUrl: undefined,
        });
        if (USE_DEXIE_DB) {
          await updateAccountSettings(this.activeDid, {
            profileImageUrl: undefined,
          });
        }

        this.profileImageUrl = undefined;

        // 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.",
          },
          3000,
        );
      }
    }
  }

  onMapReady(map: L.Map) {
    // doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
    const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
    map.setView([this.userProfileLatitude, this.userProfileLongitude], zoom);
  }

  showProfileInfo() {
    this.$notify(
      {
        group: "alert",
        type: "info",
        title: "Public Profile Information",
        text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.",
      },
      7000,
    );
  }

  async saveProfile() {
    this.savingProfile = true;
    try {
      const headers = await getHeaders(this.activeDid);
      const payload: UserProfile = {
        description: this.userProfileDesc,
      };
      if (this.userProfileLatitude && this.userProfileLongitude) {
        payload.locLat = this.userProfileLatitude;
        payload.locLon = this.userProfileLongitude;
      } else if (this.includeUserProfileLocation) {
        this.$notify(
          {
            group: "alert",
            type: "toast",
            title: "",
            text: "No profile location is saved.",
          },
          3000,
        );
      }
      const response = await this.axios.post(
        this.partnerApiServer + "/api/partner/userProfile",
        payload,
        { headers },
      );
      if (response.status === 201) {
        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Profile Saved",
            text: "Your profile has been updated successfully.",
          },
          3000,
        );
      } else {
        // won't get here because axios throws an error on non-success
        throw Error("Profile not saved");
      }
    } catch (error) {
      databaseUtil.logConsoleAndDb(
        "Error saving profile: " + errorStringForLog(error),
      );
      if (USE_DEXIE_DB) {
        logConsoleAndDb("Error saving profile: " + errorStringForLog(error));
      }
      const errorMessage: string =
        error.response?.data?.error?.message ||
        error.response?.data?.error ||
        error.message ||
        "There was an error saving your profile.";
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Saving Profile",
          text: errorMessage,
        },
        3000,
      );
    } finally {
      this.savingProfile = false;
    }
  }

  toggleUserProfileLocation() {
    this.includeUserProfileLocation = !this.includeUserProfileLocation;
    if (!this.includeUserProfileLocation) {
      this.userProfileLatitude = 0;
      this.userProfileLongitude = 0;
      this.zoom = 2;
    }
  }

  confirmEraseLatLong() {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Erase Marker",
        text: "Are you sure you don't want to mark a location? This will erase the current location.",
        onYes: async () => {
          this.eraseLatLong();
        },
      },
      -1,
    );
  }

  eraseLatLong() {
    this.userProfileLatitude = 0;
    this.userProfileLongitude = 0;
    this.zoom = 2;
    this.includeUserProfileLocation = false;
  }

  async confirmDeleteProfile() {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Delete Profile",
        text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
        onYes: this.deleteProfile,
      },
      -1,
    );
  }

  async deleteProfile() {
    this.savingProfile = true;
    try {
      const headers = await getHeaders(this.activeDid);
      const response = await this.axios.delete(
        this.partnerApiServer + "/api/partner/userProfile",
        { headers },
      );
      if (response.status === 204) {
        this.userProfileDesc = "";
        this.userProfileLatitude = 0;
        this.userProfileLongitude = 0;
        this.includeUserProfileLocation = false;
        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Profile Deleted",
            text: "Your profile has been deleted successfully.",
          },
          3000,
        );
      } else {
        throw Error("Profile not deleted");
      }
    } catch (error) {
      databaseUtil.logConsoleAndDb(
        "Error deleting profile: " + errorStringForLog(error),
      );
      if (USE_DEXIE_DB) {
        logConsoleAndDb("Error deleting profile: " + errorStringForLog(error));
      }
      const errorMessage: string =
        error.response?.data?.error?.message ||
        error.response?.data?.error ||
        error.message ||
        "There was an error deleting your profile.";
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Deleting Profile",
          text: errorMessage,
        },
        3000,
      );
    } finally {
      this.savingProfile = false;
    }
  }

  private handleQRCodeClick() {
    if (Capacitor.isNativePlatform()) {
      this.$router.push({ name: "contact-qr-scan-full" });
    } else {
      this.$router.push({ name: "contact-qr" });
    }
  }
}
</script>
