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

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

    <div class="flex justify-between">
      <span />
      <span class="whitespace-nowrap">
        <router-link
          :to="{ name: 'contact-qr' }"
          class="text-xs 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"
      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 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 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 -->
    <div
      id="sectionIdentityDetails"
      class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"
    >
      <div v-if="givenName">
        <h2 class="text-xl font-semibold mb-2">
          {{ givenName }}
          <router-link :to="{ name: 'new-edit-account' }">
            <fa icon="pen" class="text-xs text-blue-500 ml-2 mb-1"></fa>
          </router-link>
        </h2>
      </div>
      <span
        v-else
        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"
      >
        <router-link
          :to="{ name: 'new-edit-account' }"
          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"
        >
          Set Your Name
        </router-link>
      </span>
      <div class="flex justify-center mt-4">
        <span v-if="profileImageUrl" class="flex justify-between">
          <EntityIcon
            :icon-size="96"
            :profileImageUrl="profileImageUrl"
            class="inline-block align-text-bottom border border-slate-300 rounded"
            @click="showLargeIdenticonUrl = profileImageUrl"
          />
          <fa
            icon="trash-can"
            @click="confirmDeleteImage"
            class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
          />
        </span>
        <div v-else class="text-center">
          <div class @click="openImageDialog()">
            <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-l"
            />
            <fa
              icon="image-portrait"
              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-r"
              @click="openImageDialog()"
            />
          </div>
        </div>
        <ImageMethodDialog ref="imageMethodDialog" />
      </div>
      <div class="mt-6">
        <div class="flex justify-center text-center">
          People {{ profileImageUrl ? "without your image" : "" }} see this
          <br />
          (if you've let them see your activity):
        </div>
        <div class="flex justify-center">
          <EntityIcon
            :entityId="activeDid"
            :iconSize="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
            :entityId="showLargeIdenticonId"
            :iconSize="512"
            :profileImageUrl="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-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 class="text-blue-500 text-sm font-bold">
        <router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }">
          Your Activity
        </router-link>
      </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"
      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 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-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"
      >
        Share Your Info
      </router-link>
    </div>

    <div
      id="sectionNotifications"
      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
      id="sectionSearchLocation"
      class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
    >
      <!-- label -->
      <div class="mb-2 font-bold">Location for Searches</div>
      <router-link
        :to="{ name: 'search-area' }"
        class="block w-full text-center text-m 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-2 mt-6"
      >
        Set Search Area…
        <!-- If already set, change button label to "Change Search Area" -->
      </router-link>
    </div>

    <div
      v-if="activeDid"
      id="sectionUsageLimits"
      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 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
      id="sectionDataExport"
      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 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-2 mt-2"
      >
        Backup Identifier Seed
      </router-link>

      <button
        v-bind:class="computedStartDownloadLinkClassNames()"
        class="block w-full 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"
        @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 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>
        <p>
          After the download, you can save the file in your preferred storage
          location.
        </p>
        <ul>
          <li class="list-disc list-outside ml-4">
            On iOS: Choose "More..." and select a place in iCloud, or go "Back"
            and save to another location.
          </li>
          <li class="list-disc list-outside ml-4">
            On Android: Choose "Open" and then share
            <fa icon="share-nodes" class="fa-fw" />
            to your prefered place.
          </li>
        </ul>
      </div>
    </div>

    <!-- id used by puppeteer test script -->
    <h3
      id="advanced"
      class="text-sm uppercase font-semibold mb-3"
      @click="showAdvanced = !showAdvanced"
    >
      Advanced
    </h3>
    <div id="sectionAdvanced" v-if="showAdvanced || showGeneralAdvanced">
      <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
        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
            @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
          v-if="derivationPath"
          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
          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 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>

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

        <div class="ml-4 mt-2">
          <input type="file" @change="uploadImportFile" class="ml-2" />
          <div v-if="showContactImport()" class="mt-4">
            <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>
            <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
              <br />
              after comparing
            </button>
          </div>
        </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
            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 id="sectionClaimServer">
        <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 id="sectionNotificationPushServer" 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 id="sectionImageServerURL" 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
            type="checkbox"
            v-model="hideRegisterPromptOnNewContact"
            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 type="checkbox" v-model="showShortcutBvc" 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 class="flex mt-4">
        <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 mb-2"
          >
            See Global Animated History of Giving
          </router-link>
        </button>
      </div>

      <div id="sectionPasskeyExpiration" class="flex justify-between">
        <span>
          <span class="text-slate-500 text-sm font-bold mb-2">
            Passkey Expiration Minutes
          </span>
          <br />
          <span class="text-sm ml-2">
            {{ passkeyExpirationDescription }}
          </span>
        </span>
        <div class="relative ml-2">
          <input
            type="number"
            class="border border-slate-400 rounded px-2 py-2 text-center w-20"
            v-model="passkeyExpirationMinutes"
            @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
            type="checkbox"
            v-model="showGeneralAdvanced"
            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>
  </section>
</template>

<script lang="ts">
import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie";
import "dexie-export-import";
import { ImportProgress } from "dexie-export-import/dist/import";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";

import EntityIcon from "@/components/EntityIcon.vue";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import {
  AppString,
  DEFAULT_IMAGE_API_SERVER,
  DEFAULT_PUSH_SERVER,
  IMAGE_TYPE_PROFILE,
  NotificationIface,
} from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import {
  DEFAULT_PASSKEY_EXPIRATION_MINUTES,
  MASTER_SETTINGS_KEY,
  Settings,
} from "@/db/tables/settings";
import {
  clearPasskeyToken,
  ErrorResponse,
  EndorserRateLimits,
  fetchEndorserRateLimits,
  fetchImageRateLimits,
  getHeaders,
  ImageRateLimits,
  tokenExpiryTimeDescription,
} from "@/libs/endorserServer";
import { getAccount } from "@/libs/util";

const inputImportFileNameRef = ref<Blob>();

@Component({
  components: { EntityIcon, ImageMethodDialog, 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 = "";
  hideRegisterPromptOnNewContact = false;
  imageLimits: ImageRateLimits | null = null;
  imageServer = "";
  isRegistered = false;
  isSubscribed = false;
  limitsMessage = "";
  loadingLimits = false;
  notificationMaybeChanged = false;
  passkeyExpirationDescription = "";
  passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
  previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
  profileImageUrl?: string;
  publicHex = "";
  publicBase64 = "";
  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 = "";
  webPushServerInput = "";

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

      this.passkeyExpirationDescription = tokenExpiryTimeDescription();

      /**
       * Beware! I've seen where this "ready" never resolves.
       */
      const registration = await navigator.serviceWorker.ready;
      this.subscription = await registration.pushManager.getSubscription();
      this.isSubscribed = !!this.subscription;
      console.log("Got to the end of 'mounted' call.");
      /**
       * Beware! I've seen where we never get to this point because "ready" never resolves.
       */
    } catch (error) {
      console.error(
        "Telling user to clear cache at page create because:",
        error,
      );
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Loading Account",
          text: "Clear your cache and start over (after data backup).",
        },
        -1,
      );
    }
  }

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

  /**
   * Initializes component state with values from the database or defaults.
   */
  async initializeState() {
    await db.open();
    const settings: Settings | undefined =
      await db.settings.get(MASTER_SETTINGS_KEY);

    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.imageServer = (settings?.imageServer as string) || "";
    this.profileImageUrl = settings?.profileImageUrl as string;
    this.showContactGives = !!settings?.showContactGivesInline;
    this.hideRegisterPromptOnNewContact =
      !!settings?.hideRegisterPromptOnNewContact;
    this.passkeyExpirationMinutes =
      (settings?.passkeyExpirationMinutes as number) ??
      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 as string) || "";
    this.webPushServerInput = (settings?.webPushServer as string) || "";
  }

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

  toggleShowGeneralAdvanced() {
    this.showGeneralAdvanced = !this.showGeneralAdvanced;
    this.updateShowGeneralAdvanced();
  }

  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.
   */
  async processIdentity() {
    const account: Account | undefined = await getAccount(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.checkLimitsFor(this.activeDid);
    } else if (account?.publicKeyHex) {
      this.publicHex = account.publicKeyHex as string;
      this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
      await this.checkLimitsFor(this.activeDid);
    }
  }

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

  public async updateShowContactAmounts() {
    await db.open();
    await db.settings.update(MASTER_SETTINGS_KEY, {
      showContactGivesInline: this.showContactGives,
    });
  }

  public async updateShowGeneralAdvanced() {
    await db.open();
    await db.settings.update(MASTER_SETTINGS_KEY, {
      showGeneralAdvanced: this.showGeneralAdvanced,
    });
  }

  public async updateWarnIfProdServer(newSetting: boolean) {
    try {
      await db.open();
      await 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) {
    await db.open();
    await db.settings.update(MASTER_SETTINGS_KEY, {
      warnIfTestServer: newSetting,
    });
  }

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

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

  public async updateShowShortcutBvc(newSetting: boolean) {
    await db.open();
    await db.settings.update(MASTER_SETTINGS_KEY, {
      showShortcutBvc: newSetting,
    });
  }

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

  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) {
      await db.delete();
      await Dexie.import(inputImportFileNameRef.value as Blob, {
        progressCallback: this.progressCallback,
      });
    }
  }

  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) {
        console.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) {
    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() {
    if (this.activeDid) {
      this.checkLimitsFor(this.activeDid);
    } 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(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 db.open();
            await 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: "danger",
                title: "Update Error",
                text: "Unable to update your settings. Check claim limits again.",
              },
              -1,
            );
          }
        }
        const imageResp = await fetchImageRateLimits(this.axios, did);
        if (imageResp.status === 200) {
          this.imageLimits = imageResp.data;
        }
      }
    } catch (error) {
      this.handleRateLimitsError(error);

      try {
        await db.open();
        await 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;
  }

  /**
   * 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.",
        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: Account) {
    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();
    await db.settings.update(MASTER_SETTINGS_KEY, {
      apiServer: this.apiServerInput,
    });
    this.apiServer = this.apiServerInput;
  }

  async onClickSavePushServer() {
    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.",
      },
      -1,
    );
  }

  openImageDialog() {
    (this.$refs.imageMethodDialog as ImageMethodDialog).open(
      async (imgUrl) => {
        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();
      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 {
        console.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.",
          },
          -1,
        );
        // keep the imageUrl in localStorage so the user can try again if they want
      }

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

      this.profileImageUrl = undefined;
    } 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.error("The image was already deleted:", error);

        await db.open();
        await db.settings.update(MASTER_SETTINGS_KEY, {
          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.",
          },
          5000,
        );
      }
    }
  }
}
</script>
