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

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

    <div class="flex justify-between py-2 mt-8">
      <span />
      <span>
        <a
          :href="APP_SERVER + '/help-onboarding'"
          target="_blank"
          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"
        >
          Onboarding Guide
        </a>
      </span>
    </div>

    <!-- New Contact -->
    <div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
      <span v-if="isRegistered" class="flex">
        <router-link
          :to="{ name: 'invite-one' }"
          class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
        >
          <font-awesome icon="envelope-open-text" class="fa-fw text-2xl" />
        </router-link>

        <button
          class="flex items-center bg-gradient-to-b from-green-400 to-green-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="showOnboardMeetingDialog()"
        >
          <font-awesome icon="chair" class="fa-fw text-2xl" />
        </button>
      </span>
      <span v-else class="flex">
        <span
          class="flex items-center 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"
        >
          <font-awesome
            icon="envelope-open-text"
            class="fa-fw text-2xl"
            @click="
              warning(
                'You must get registered before you can create invites.',
                'Not Registered',
              )
            "
          />
        </span>
        <span
          class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
        >
          <font-awesome
            icon="chair"
            class="fa-fw text-2xl"
            @click="$router.push({ name: 'onboard-meeting-list' })"
          />
        </span>
      </span>

      <button
        class="flex items-center bg-gradient-to-b from-green-400 to-green-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-2xl" />
      </button>

      <textarea
        v-model="contactInput"
        type="text"
        placeholder="New URL or DID, Name, Public Key, Next Public Key Hash"
        class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
      />
      <button
        class="px-4 rounded-r bg-green-200 border border-green-400"
        @click="onClickNewContact()"
      >
        <font-awesome icon="plus" class="fa-fw" />
      </button>
    </div>

    <div v-if="contacts.length > 0" class="flex justify-between">
      <div class="">
        <div v-if="!showGiveNumbers" class="flex items-center">
          <input
            type="checkbox"
            :checked="contactsSelected.length === contacts.length"
            class="align-middle ml-2 h-6 w-6"
            data-testId="contactCheckAllTop"
            @click="
              contactsSelected.length === contacts.length
                ? (contactsSelected = [])
                : (contactsSelected = contacts.map((contact) => contact.did))
            "
          />
          <button
            v-if="!showGiveNumbers"
            :class="
              contactsSelected.length > 0
                ? '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 ' +
                  'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
                : '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-slate-300 ' +
                  'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
            "
            data-testId="copySelectedContactsButtonTop"
            @click="copySelectedContacts()"
          >
            Copy
          </button>
          <font-awesome
            icon="circle-info"
            class="text-2xl text-blue-500 ml-2"
            @click="showCopySelectionsInfo()"
          />
        </div>
      </div>

      <div class="flex items-center gap-2">
        <button
          v-if="showGiveNumbers"
          class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
          :class="showGiveAmountsClassNames()"
          @click="toggleShowGiveTotals()"
        >
          {{
            showGiveTotals
              ? "Totals"
              : showGiveConfirmed
                ? "Confirmed Amounts"
                : "Unconfirmed Amounts"
          }}
          <font-awesome icon="left-right" class="fa-fw" />
        </button>

        <button
          class="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-3 py-1.5 rounded-md"
          @click="toggleShowContactAmounts()"
        >
          {{ showGiveNumbers ? "Hide Actions" : "See Actions" }}
        </button>
      </div>
    </div>
    <div v-if="showGiveNumbers" class="my-3">
      <div class="w-full text-center text-sm italic text-slate-600">
        Only the most recent hours are included. <br />To see more, click
        <span
          class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-0.5 rounded"
        >
          <font-awesome icon="file-lines" class="text-xs fa-fw" />
        </span>
        <br />
      </div>
    </div>

    <!-- Results List -->
    <ul
      v-if="contacts.length > 0"
      id="listContacts"
      class="border-t border-slate-300 my-2"
    >
      <li
        v-for="contact in filteredContacts()"
        :key="contact.did"
        class="border-b border-slate-300 pt-1 pb-1"
        data-testId="contactListItem"
      >
        <div class="flex items-center justify-between gap-3">
          <div class="flex overflow-hidden min-w-0 items-center gap-3">
            <input
              v-if="!showGiveNumbers"
              type="checkbox"
              :checked="contactsSelected.includes(contact.did)"
              class="ml-2 h-6 w-6 flex-shrink-0"
              data-testId="contactCheckOne"
              @click="
                contactsSelected.includes(contact.did)
                  ? contactsSelected.splice(
                      contactsSelected.indexOf(contact.did),
                      1,
                    )
                  : contactsSelected.push(contact.did)
              "
            />

            <EntityIcon
              :contact="contact"
              :icon-size="48"
              class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
              @click="showLargeIdenticon = contact"
            />

            <div class="overflow-hidden">
              <h2 class="text-base font-semibold truncate">
                <router-link
                  :to="{
                    path: '/did/' + encodeURIComponent(contact.did),
                  }"
                  title="See more about this person"
                >
                  {{ contactNameNonBreakingSpace(contact.name) }}
                </router-link>
              </h2>

              <div class="flex gap-1.5 items-center overflow-hidden">
                <router-link
                  :to="{
                    path: '/did/' + encodeURIComponent(contact.did),
                  }"
                  title="See more about this person"
                >
                  <font-awesome
                    icon="circle-info"
                    class="text-base text-blue-500"
                  />
                </router-link>

                <span class="text-xs truncate">{{ contact.did }}</span>
              </div>
              <div class="text-sm">
                {{ contact.notes }}
              </div>
            </div>
          </div>

          <div
            v-if="showGiveNumbers && contact.did != activeDid"
            class="flex gap-1.5 items-end"
          >
            <div class="text-center">
              <div class="text-xs leading-none mb-1">From/To</div>
              <div class="flex items-center">
                <button
                  class="text-sm 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.5 py-1.5 rounded-l-md"
                  :title="givenToMeDescriptions[contact.did] || ''"
                  @click="confirmShowGiftedDialog(contact.did, activeDid)"
                >
                  {{
                    /* eslint-disable prettier/prettier */
                    showGiveTotals
                      ? ((givenToMeConfirmed[contact.did] || 0)
                          + (givenToMeUnconfirmed[contact.did] || 0))
                      : showGiveConfirmed
                          ? (givenToMeConfirmed[contact.did] || 0)
                          : (givenToMeUnconfirmed[contact.did] || 0)
                    /* eslint-enable prettier/prettier */
                  }}
                </button>

                <button
                  class="text-sm 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.5 py-1.5 rounded-r-md border-l"
                  :title="givenByMeDescriptions[contact.did] || ''"
                  @click="confirmShowGiftedDialog(activeDid, contact.did)"
                >
                  {{
                    /* eslint-disable prettier/prettier */
                    showGiveTotals
                      ? ((givenByMeConfirmed[contact.did] || 0)
                        + (givenByMeUnconfirmed[contact.did] || 0))
                      : showGiveConfirmed
                          ? (givenByMeConfirmed[contact.did] || 0)
                          : (givenByMeUnconfirmed[contact.did] || 0)
                    /* eslint-enable prettier/prettier */
                  }}
                </button>
              </div>
            </div>

            <button
              class="text-sm 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-1.5 rounded-md"
              data-testId="offerButton"
              @click="openOfferDialog(contact.did, contact.name)"
            >
              Offer
            </button>

            <router-link
              :to="{
                name: 'contact-amounts',
                query: { contactDid: contact.did },
              }"
              class="text-sm 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-2 py-1.5 rounded-md"
              title="See more given activity"
            >
              <font-awesome icon="file-lines" class="fa-fw" />
            </router-link>
          </div>
        </div>
      </li>
    </ul>
    <p v-else>There are no contacts.</p>

    <div v-if="contacts.length > 0" class="mt-2 w-full text-left">
      <input
        v-if="!showGiveNumbers"
        type="checkbox"
        :checked="contactsSelected.length === contacts.length"
        class="align-middle ml-2 h-6 w-6"
        data-testId="contactCheckAllBottom"
        @click="
          contactsSelected.length === contacts.length
            ? (contactsSelected = [])
            : (contactsSelected = contacts.map((contact) => contact.did))
        "
      />
      <button
        v-if="!showGiveNumbers"
        :class="
          contactsSelected.length > 0
            ? '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 ' +
              'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
            : '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-slate-300 ' +
              'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
        "
        @click="copySelectedContacts()"
      >
        Copy
      </button>
    </div>

    <GiftedDialog ref="customGivenDialog" />
    <OfferDialog ref="customOfferDialog" />
    <ContactNameDialog ref="contactNameDialog" />

    <div v-if="showLargeIdenticon" 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
          :contact="showLargeIdenticon"
          :icon-size="512"
          class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
          @click="showLargeIdenticon = undefined"
        />
      </div>
    </div>
  </section>
</template>

<script lang="ts">
import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import { IndexableType } from "dexie";
import { JWTPayload } from "did-jwt";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { Capacitor } from "@capacitor/core";

import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue";
import GiftedDialog from "../components/GiftedDialog.vue";
import OfferDialog from "../components/OfferDialog.vue";
import ContactNameDialog from "../components/ContactNameDialog.vue";
import TopMessage from "../components/TopMessage.vue";
import {
  APP_SERVER,
  AppString,
  NotificationIface,
  USE_DEXIE_DB,
} from "../constants/app";
import {
  db,
  logConsoleAndDb,
  retrieveSettingsForActiveAccount,
  updateAccountSettings,
  updateDefaultSettings,
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";
import {
  CONTACT_CSV_HEADER,
  createEndorserJwtForDid,
  errorStringForLog,
  GiveSummaryRecord,
  getHeaders,
  isDid,
  register,
  setVisibilityUtil,
  UserInfo,
  VerifiableCredential,
  CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
  CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
  CONTACT_URL_PATH_ENDORSER_CH_OLD,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { generateSaveAndActivateIdentity } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({
  components: {
    GiftedDialog,
    EntityIcon,
    OfferDialog,
    QuickNav,
    ContactNameDialog,
    TopMessage,
  },
})
export default class ContactsView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  $route!: RouteLocationNormalizedLoaded;
  $router!: Router;

  activeDid = "";
  apiServer = "";
  contacts: Array<Contact> = [];
  contactInput = "";
  contactEdit: Contact | null = null;
  contactNewName = "";
  contactsSelected: Array<string> = [];
  // { "did:...": concatenated-descriptions } entry for each contact
  givenByMeDescriptions: Record<string, string> = {};
  // { "did:...": amount } entry for each contact
  givenByMeConfirmed: Record<string, number> = {};
  // { "did:...": amount } entry for each contact
  givenByMeUnconfirmed: Record<string, number> = {};
  // { "did:...": concatenated-descriptions } entry for each contact
  givenToMeDescriptions: Record<string, string> = {};
  // { "did:...": amount } entry for each contact
  givenToMeConfirmed: Record<string, number> = {};
  // { "did:...": amount } entry for each contact
  givenToMeUnconfirmed: Record<string, number> = {};
  hideRegisterPromptOnNewContact = false;
  isRegistered = false;
  showDidCopy = false;
  showPubKeyCopy = false;
  showPubKeyHashCopy = false;
  showGiveNumbers = false;
  showGiveTotals = true;
  showGiveConfirmed = true;
  showLargeIdenticon?: Contact;

  APP_SERVER = APP_SERVER;
  AppString = AppString;
  libsUtil = libsUtil;

  public async created() {
    let settings = await databaseUtil.retrieveSettingsForActiveAccount();
    if (USE_DEXIE_DB) {
      await db.open();
      settings = await retrieveSettingsForActiveAccount();
    }
    this.activeDid = settings.activeDid || "";
    this.apiServer = settings.apiServer || "";
    this.isRegistered = !!settings.isRegistered;

    // if these detect a query parameter, they can and then redirect to this URL without a query parameter
    // to avoid problems when they reload or they go forward & back and it tries to reprocess
    await this.processContactJwt();
    await this.processInviteJwt();

    this.showGiveNumbers = !!settings.showContactGivesInline;
    this.hideRegisterPromptOnNewContact =
      !!settings.hideRegisterPromptOnNewContact;

    if (this.showGiveNumbers) {
      this.loadGives();
    }

    const platformService = PlatformServiceFactory.getInstance();
    const dbAllContacts = await platformService.dbQuery(
      "SELECT * FROM contacts ORDER BY name",
    );
    this.contacts = databaseUtil.mapQueryResultToValues(
      dbAllContacts,
    ) as unknown as Contact[];
    if (USE_DEXIE_DB) {
      await db.open();
      // .orderBy("name") wouldn't retrieve any entries with a blank name
      // .toCollection.sortBy("name") didn't sort in an order I understood
      const baseContacts = await db.contacts.toArray();
      this.contacts = baseContacts.sort((a, b) =>
        (a.name || "").localeCompare(b.name || ""),
      );
    }
  }

  private async processContactJwt() {
    // handle a contact sent via URL
    //
    // For external links, use /deep-link/contact-import/:jwt with a JWT that has an array of contacts
    // because that will do better error checking for things like missing data on iOS platforms.
    const importedContactJwt = this.$route.query["contactJwt"] as string;
    if (importedContactJwt) {
      // really should fully verify contents
      const { payload } = decodeEndorserJwt(importedContactJwt);
      const userInfo = payload["own"] as UserInfo;
      const newContact = {
        did: userInfo.did || payload["iss"], // ".did" is reliable as of v 0.3.49
        name: userInfo.name,
        nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
        profileImageUrl: userInfo.profileImageUrl,
        publicKeyBase64: userInfo.publicEncKey,
        registered: userInfo.registered,
      } as Contact;
      await this.addContact(newContact);
      // if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
      this.$router.push({ path: "/contacts" });
    }
  }

  private async processInviteJwt() {
    // handle an invite JWT sent via URL
    const importedInviteJwt = this.$route.query["inviteJwt"] as string;
    if (importedInviteJwt === "") {
      // this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Blank Invite",
          text: "The invite was not included, which can happen when your iOS device cuts off the link. Try pasting the full link into a browser.",
        },
        7000,
      );
    } else if (importedInviteJwt) {
      // make sure user is created
      if (!this.activeDid) {
        this.activeDid = await generateSaveAndActivateIdentity();
      }
      // send invite directly to server, with auth for this user
      const headers = await getHeaders(this.activeDid);
      try {
        const response = await this.axios.post(
          this.apiServer + "/api/v2/claim",
          { jwtEncoded: importedInviteJwt },
          { headers },
        );
        if (response.status != 201) {
          throw { error: { response: response } };
        }
        await databaseUtil.updateDidSpecificSettings(this.activeDid, {
          isRegistered: true,
        });
        if (USE_DEXIE_DB) {
          await updateAccountSettings(this.activeDid, { isRegistered: true });
        }
        this.isRegistered = true;
        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Registered",
            text: "You are now registered.",
          },
          3000,
        );

        // wait for a second before continuing so they see the registration message
        await new Promise((resolve) => setTimeout(resolve, 1000));

        // now add the inviter as a contact
        // (similar code is in InviteOneAcceptView.vue)
        const payload: JWTPayload =
          decodeEndorserJwt(importedInviteJwt).payload;
        const registration = payload as VerifiableCredential;
        (this.$refs.contactNameDialog as ContactNameDialog).open(
          "Who Invited You?",
          "",
          async (name) => {
            await this.addContact({
              did: registration.vc.credentialSubject.agent.identifier,
              name: name,
              registered: true,
            });
            // wait for a second before continuing so they see the user-added message
            await new Promise((resolve) => setTimeout(resolve, 1000));
            this.showOnboardingInfo();
          },
          async () => {
            // on cancel, will still add the contact
            await this.addContact({
              did: registration.vc.credentialSubject.agent.identifier,
              name: "(person who invited you)",
              registered: true,
            });
            // wait for a second before continuing so they see the user-added message
            await new Promise((resolve) => setTimeout(resolve, 1000));
            this.showOnboardingInfo();
          },
        );
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
      } catch (error: any) {
        const fullError = "Error redeeming invite: " + errorStringForLog(error);
        logConsoleAndDb(fullError, true);
        let message = "Got an error sending the invite.";
        if (
          error.response &&
          error.response.data &&
          error.response.data.error
        ) {
          if (error.response.data.error.message) {
            message = error.response.data.error.message;
          } else {
            message = error.response.data.error;
          }
        } else if (error.message) {
          message = error.message;
        }
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error with Invite",
            text: message,
          },
          -1,
        );
      }
      // if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
      this.$router.push({ path: "/contacts" });
    }
  }

  private contactNameNonBreakingSpace(contactName?: string) {
    return (contactName || AppString.NO_CONTACT_NAME).replace(/\s/g, "\u00A0");
  }

  private danger(message: string, title: string = "Error", timeout = 5000) {
    this.$notify(
      {
        group: "alert",
        type: "danger",
        title: title,
        text: message,
      },
      timeout,
    );
  }

  private warning(message: string, title: string = "Error", timeout = 5000) {
    this.$notify(
      {
        group: "alert",
        type: "warning",
        title: title,
        text: message,
      },
      timeout,
    );
  }

  private showOnboardingInfo() {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "They're Added To Your List",
        text: "Would you like to go to the main page now?",
        onYes: async () => {
          this.$router.push({ name: "home" });
        },
      },
      -1,
    );
  }

  private filteredContacts() {
    return this.showGiveNumbers
      ? this.contactsSelected.length === 0
        ? this.contacts
        : this.contacts.filter((contact) =>
            this.contactsSelected.includes(contact.did),
          )
      : this.contacts;
  }

  private async loadGives() {
    if (!this.activeDid) {
      return;
    }

    const handleResponse = (
      resp: { status: number; data: { data: GiveSummaryRecord[] } },
      descriptions: Record<string, string>,
      confirmed: Record<string, number>,
      unconfirmed: Record<string, number>,
      useRecipient: boolean,
    ) => {
      if (resp.status === 200) {
        const allData = resp.data.data;
        for (const give of allData) {
          const otherDid = useRecipient ? give.recipientDid : give.agentDid;
          if (give.unit === "HUR") {
            if (give.amountConfirmed) {
              const prevAmount = confirmed[otherDid] || 0;
              confirmed[otherDid] = prevAmount + give.amount;
            } else {
              const prevAmount = unconfirmed[otherDid] || 0;
              unconfirmed[otherDid] = prevAmount + give.amount;
            }
            if (!descriptions[otherDid] && give.description) {
              descriptions[otherDid] = give.description;
            }
          }
        }
      } else {
        logger.error(
          "Got bad response status & data of",
          resp.status,
          resp.data,
        );
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Retrieval Error",
            text:
              "Got an error retrieving your " +
              (useRecipient ? "given" : "received") +
              " data from the server.",
          },
          3000,
        );
      }
    };

    try {
      const headers = await getHeaders(this.activeDid, this.$notify);
      const givenByUrl =
        this.apiServer +
        "/api/v2/report/gives?agentDid=" +
        encodeURIComponent(this.activeDid);
      const givenToUrl =
        this.apiServer +
        "/api/v2/report/gives?recipientDid=" +
        encodeURIComponent(this.activeDid);

      const [givenByMeResp, givenToMeResp] = await Promise.all([
        this.axios.get(givenByUrl, { headers }),
        this.axios.get(givenToUrl, { headers }),
      ]);

      const givenByMeDescriptions = {};
      const givenByMeConfirmed = {};
      const givenByMeUnconfirmed = {};
      handleResponse(
        givenByMeResp,
        givenByMeDescriptions,
        givenByMeConfirmed,
        givenByMeUnconfirmed,
        true,
      );
      this.givenByMeDescriptions = givenByMeDescriptions;
      this.givenByMeConfirmed = givenByMeConfirmed;
      this.givenByMeUnconfirmed = givenByMeUnconfirmed;

      const givenToMeDescriptions = {};
      const givenToMeConfirmed = {};
      const givenToMeUnconfirmed = {};
      handleResponse(
        givenToMeResp,
        givenToMeDescriptions,
        givenToMeConfirmed,
        givenToMeUnconfirmed,
        false,
      );
      this.givenToMeDescriptions = givenToMeDescriptions;
      this.givenToMeConfirmed = givenToMeConfirmed;
      this.givenToMeUnconfirmed = givenToMeUnconfirmed;
    } catch (error) {
      const fullError = "Error loading gives: " + errorStringForLog(error);
      logConsoleAndDb(fullError, true);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Load Error",
          text: "Got an error loading your gives.",
        },
        3000,
      );
    }
  }

  private async onClickNewContact(): Promise<void> {
    const contactInput = this.contactInput.trim();
    if (!contactInput) {
      this.danger(
        "There was no contact info to add. Try the other green buttons.",
        "No Contact",
      );
      return;
    }

    if (contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
      const jwt = getContactJwtFromJwtUrl(contactInput);
      this.$router.push({ path: "/contact-import/" + jwt });
      return;
    }

    if (
      contactInput.includes(CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI) ||
      contactInput.includes(CONTACT_URL_PATH_ENDORSER_CH_OLD)
    ) {
      const jwt = getContactJwtFromJwtUrl(contactInput);
      const { payload } = decodeEndorserJwt(jwt);
      const userInfo = payload["own"] as UserInfo;
      const newContact = {
        did: userInfo.did || payload["iss"], // "did" is reliable as of v 0.3.49
        name: userInfo.name,
        nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
        profileImageUrl: userInfo.profileImageUrl,
        publicKeyBase64: userInfo.publicEncKey,
        registered: userInfo.registered,
      } as Contact;
      await this.addContact(newContact);
      return;
    }

    if (contactInput.startsWith(CONTACT_CSV_HEADER)) {
      const lines = contactInput.split(/\n/);
      const lineAdded = [];
      for (const line of lines) {
        if (!line.trim() || line.startsWith(CONTACT_CSV_HEADER)) {
          continue;
        }
        lineAdded.push(this.addContactFromEndorserMobileLine(line));
      }
      try {
        await Promise.all(lineAdded);
        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Contacts Added",
            text: "Each contact was added. Nothing was sent to the server.",
          },
          3000, // keeping it up so that the "visibility" message is seen
        );
      } catch (e) {
        const fullError =
          "Error adding contacts from CSV: " + errorStringForLog(e);
        logConsoleAndDb(fullError, true);
        this.danger("An error occurred. Some contacts may have been added.");
      }

      const platformService = PlatformServiceFactory.getInstance();
      const dbAllContacts = await platformService.dbQuery(
        "SELECT * FROM contacts ORDER BY name",
      );
      this.contacts = databaseUtil.mapQueryResultToValues(
        dbAllContacts,
      ) as unknown as Contact[];
      if (USE_DEXIE_DB) {
        await db.open();
        // .orderBy("name") wouldn't retrieve any entries with a blank name
        // .toCollection.sortBy("name") didn't sort in an order I understood
        const baseContacts = await db.contacts.toArray();
        this.contacts = baseContacts.sort((a, b) =>
          (a.name || "").localeCompare(b.name || ""),
        );
      }
      return;
    }

    if (contactInput.startsWith("did:")) {
      let did = contactInput;
      let name, publicKeyInput, nextPublicKeyHashInput;
      const commaPos1 = contactInput.indexOf(",");
      if (commaPos1 > -1) {
        did = contactInput.substring(0, commaPos1).trim();
        name = contactInput.substring(commaPos1 + 1).trim();
        const commaPos2 = contactInput.indexOf(",", commaPos1 + 1);
        if (commaPos2 > -1) {
          name = contactInput.substring(commaPos1 + 1, commaPos2).trim();
          publicKeyInput = contactInput.substring(commaPos2 + 1).trim();
          const commaPos3 = contactInput.indexOf(",", commaPos2 + 1);
          if (commaPos3 > -1) {
            publicKeyInput = contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
            nextPublicKeyHashInput = contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier
          }
        }
      }
      // help with potential mistakes while this sharing requires copy-and-paste
      let publicKeyBase64 = publicKeyInput;
      if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
        // it must be all hex (compressed public key), so convert
        publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString(
          "base64",
        );
      }
      let nextPubKeyHashB64 = nextPublicKeyHashInput;
      if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) {
        // it must be all hex (compressed public key), so convert
        nextPubKeyHashB64 = Buffer.from(nextPubKeyHashB64, "hex").toString("base64"); // eslint-disable-line prettier/prettier
      }
      const newContact = {
        did,
        name,
        publicKeyBase64,
        nextPubKeyHashB64: nextPubKeyHashB64,
      };
      await this.addContact(newContact);
      return;
    }

    if (contactInput.includes("[")) {
      // assume there's a JSON array of contacts in the input
      const jsonContactInput = contactInput.substring(
        contactInput.indexOf("["),
        contactInput.lastIndexOf("]") + 1,
      );
      try {
        const contacts = JSON.parse(jsonContactInput);
        this.$router.push({
          name: "contact-import",
          query: { contacts: JSON.stringify(contacts) },
        });
      } catch (e) {
        const fullError =
          "Error adding contacts from array: " + errorStringForLog(e);
        logConsoleAndDb(fullError, true);
        this.danger("The input could not be parsed.", "Invalid Contact List");
      }
      return;
    }

    this.danger("No contact info was found in that input.", "No Contact Info");
  }

  private async addContactFromEndorserMobileLine(
    lineRaw: string,
  ): Promise<IndexableType> {
    const newContact = libsUtil.csvLineToContact(lineRaw);
    const platformService = PlatformServiceFactory.getInstance();
    const { sql, params } = databaseUtil.generateInsertStatement(
      newContact as unknown as Record<string, unknown>,
      "contacts",
    );
    await platformService.dbExec(sql, params);
    if (USE_DEXIE_DB) {
      await db.contacts.add(newContact);
    }
  }

  private async addContact(newContact: Contact) {
    if (!newContact.did) {
      this.danger("Cannot add a contact without a DID.", "Incomplete Contact");
      return;
    }
    if (!isDid(newContact.did)) {
      this.danger("The DID must begin with 'did:'", "Invalid DID");
      return;
    }

    const platformService = PlatformServiceFactory.getInstance();
    const { sql, params } = databaseUtil.generateInsertStatement(
      newContact as unknown as Record<string, unknown>,
      "contacts",
    );
    let contactPromise = platformService.dbExec(sql, params);
    if (USE_DEXIE_DB) {
      // @ts-expect-error since the result of this promise won't be used, and this will go away soon
      contactPromise = db.contacts.add(newContact);
    }
    return contactPromise
      .then(() => {
        const allContacts = this.contacts.concat([newContact]);
        this.contacts = R.sort(
          (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
          allContacts,
        );
        let addedMessage;
        if (this.activeDid) {
          this.setVisibility(newContact, true, false);
          newContact.seesMe = true; // didn't work inside setVisibility
          addedMessage =
            "They were added, and your activity is visible to them.";
        } else {
          addedMessage = "They were added.";
        }
        this.contactInput = "";
        if (this.isRegistered) {
          if (!this.hideRegisterPromptOnNewContact && !newContact.registered) {
            setTimeout(() => {
              this.$notify(
                {
                  group: "modal",
                  type: "confirm",
                  title: "Register",
                  text: "Do you want to register them?",
                  onCancel: async (stopAsking?: boolean) => {
                    if (stopAsking) {
                      await databaseUtil.updateDefaultSettings({
                        hideRegisterPromptOnNewContact: stopAsking,
                      });
                      if (USE_DEXIE_DB) {
                        await updateDefaultSettings({
                          hideRegisterPromptOnNewContact: stopAsking,
                        });
                      }
                      this.hideRegisterPromptOnNewContact = stopAsking;
                    }
                  },
                  onNo: async (stopAsking?: boolean) => {
                    if (stopAsking) {
                      await databaseUtil.updateDefaultSettings({
                        hideRegisterPromptOnNewContact: stopAsking,
                      });
                      if (USE_DEXIE_DB) {
                        await updateDefaultSettings({
                          hideRegisterPromptOnNewContact: stopAsking,
                        });
                      }
                      this.hideRegisterPromptOnNewContact = stopAsking;
                    }
                  },
                  onYes: async () => {
                    await this.register(newContact);
                  },
                  promptToStopAsking: true,
                },
                -1,
              );
            }, 1000);
          }
        }
        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Contact Added",
            text: addedMessage,
          },
          3000,
        );
      })
      .catch((err) => {
        const fullError =
          "Error when adding contact to storage: " + errorStringForLog(err);
        logConsoleAndDb(fullError, true);
        let message = "An error prevented this import.";
        if (
          err.message?.indexOf("Key already exists in the object store.") > -1
        ) {
          message =
            "A contact with that DID is already in your contact list. Edit them directly below.";
        }
        if (err.name === "ConstraintError") {
          message +=
            " Check that the contact doesn't conflict with any you already have.";
        }
        this.danger(message, "Contact Not Added", 5000);
      });
  }

  // note that this is also in DIDView.vue
  private async confirmSetVisibility(contact: Contact, visibility: boolean) {
    const visibilityPrompt = visibility
      ? "Are you sure you want to make your activity visible to them?"
      : "Are you sure you want to hide all your activity from them?";
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Set Visibility",
        text: visibilityPrompt,
        onYes: async () => {
          const success = await this.setVisibility(contact, visibility, true);
          if (success) {
            contact.seesMe = visibility; // didn't work inside setVisibility
          }
        },
      },
      -1,
    );
  }

  // note that this is also in DIDView.vue
  private async register(contact: Contact) {
    this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);

    try {
      const regResult = await register(
        this.activeDid,
        this.apiServer,
        this.axios,
        contact,
      );
      if (regResult.success) {
        contact.registered = true;
        const platformService = PlatformServiceFactory.getInstance();
        await platformService.dbExec(
          "UPDATE contacts SET registered = ? WHERE did = ?",
          [true, contact.did],
        );
        if (USE_DEXIE_DB) {
          await db.contacts.update(contact.did, { registered: true });
        }

        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Registration Success",
            text:
              (contact.name || "That unnamed person") + " has been registered.",
          },
          3000,
        );
      } else {
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Registration Error",
            text:
              (regResult.error as string) ||
              "Something went wrong during registration.",
          },
          -1,
        );
      }
    } catch (error) {
      const fullError = "Error when registering: " + errorStringForLog(error);
      logConsoleAndDb(fullError, true);
      let userMessage = "There was an error.";
      const serverError = error as AxiosError;
      if (serverError.isAxiosError) {
        if (
          serverError.response?.data &&
          typeof serverError.response.data === "object" &&
          "error" in serverError.response.data &&
          typeof serverError.response.data.error === "object" &&
          serverError.response.data.error !== null &&
          "message" in serverError.response.data.error
        ) {
          userMessage = serverError.response.data.error.message as string;
        } else if (serverError.message) {
          userMessage = serverError.message; // Info for the user
        } else {
          userMessage = JSON.stringify(serverError.toJSON());
        }
      } else {
        userMessage = error as string;
      }
      // Now set that error for the user to see.
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Registration Error",
          text: userMessage,
        },
        -1,
      );
    }
  }

  // note that this is also in DIDView.vue
  private async setVisibility(
    contact: Contact,
    visibility: boolean,
    showSuccessAlert: boolean,
  ) {
    const result = await setVisibilityUtil(
      this.activeDid,
      this.apiServer,
      this.axios,
      db,
      contact,
      visibility,
    );
    if (result.success) {
      //contact.seesMe = visibility; // why doesn't it affect the UI from here?
      if (showSuccessAlert) {
        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Visibility Set",
            text:
              (contact.name || "That user") +
              " can " +
              (visibility ? "" : "not ") +
              "see your activity.",
          },
          3000,
        );
      }
      return true;
    } else {
      logger.error(
        "Got strange result from setting visibility. It can happen when setting visibility on oneself.",
        result,
      );
      const message =
        (result.error as string) || "Could not set visibility on the server.";
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Setting Visibility",
          text: message,
        },
        5000,
      );
      return false;
    }
  }

  private confirmShowGiftedDialog(giverDid: string, recipientDid: string) {
    // if they have unconfirmed amounts, ask to confirm those
    if (
      recipientDid === this.activeDid &&
      this.givenToMeUnconfirmed[giverDid] > 0
    ) {
      const isAre = this.givenToMeUnconfirmed[giverDid] == 1 ? "is" : "are";
      const hours = this.givenToMeUnconfirmed[giverDid] == 1 ? "hour" : "hours";
      const message =
        "There " +
        isAre +
        " " +
        this.givenToMeUnconfirmed[giverDid] +
        " unconfirmed " +
        hours +
        " from them." +
        " Would you like to confirm some of those hours?";
      this.$notify(
        {
          group: "modal",
          type: "confirm",
          title: "Delete",
          text: message,
          onNo: async () => {
            this.showGiftedDialog(giverDid, recipientDid);
          },
          onYes: async () => {
            this.$router.push({
              name: "contact-amounts",
              query: { contactDid: giverDid },
            });
          },
        },
        -1,
      );
    } else {
      this.showGiftedDialog(giverDid, recipientDid);
    }
  }

  private showGiftedDialog(giverDid: string, recipientDid: string) {
    let giver: libsUtil.GiverReceiverInputInfo | undefined;
    let receiver: libsUtil.GiverReceiverInputInfo | undefined;
    if (giverDid) {
      giver = {
        did: giverDid,
        name: libsUtil.nameForDid(this.activeDid, this.contacts, giverDid),
      };
    }
    if (recipientDid) {
      receiver = {
        did: recipientDid,
        name: libsUtil.nameForDid(this.activeDid, this.contacts, recipientDid),
      };
    }

    let callback: (amount: number) => void;
    let customTitle = "";
    // choose whether to open dialog to user or from user
    if (giverDid == this.activeDid) {
      callback = (amount: number) => {
        const newList = R.clone(this.givenByMeUnconfirmed);
        newList[recipientDid] = (newList[recipientDid] || 0) + amount;
        this.givenByMeUnconfirmed = newList;
      };
      customTitle = "Given to " + (receiver?.name || "Someone Unnamed");
    } else {
      // must be (recipientDid == this.activeDid)
      callback = (amount: number) => {
        const newList = R.clone(this.givenToMeUnconfirmed);
        newList[giverDid] = (newList[giverDid] || 0) + amount;
        this.givenToMeUnconfirmed = newList;
      };
      customTitle = "Received from " + (giver?.name || "Someone Unnamed");
    }
    (this.$refs.customGivenDialog as GiftedDialog).open(
      giver,
      receiver,
      undefined as unknown as string,
      customTitle,
      undefined as unknown as string,
      callback,
    );
  }

  openOfferDialog(recipientDid: string, recipientName?: string) {
    (this.$refs.customOfferDialog as OfferDialog).open(
      recipientDid,
      recipientName,
    );
  }

  private async toggleShowContactAmounts() {
    const newShowValue = !this.showGiveNumbers;
    try {
      await databaseUtil.updateDefaultSettings({
        showContactGivesInline: newShowValue,
      });
      if (USE_DEXIE_DB) {
        await updateDefaultSettings({
          showContactGivesInline: newShowValue,
        });
      }
    } catch (err) {
      const fullError =
        "Error updating contact-amounts setting: " + errorStringForLog(err);
      logConsoleAndDb(fullError, true);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Updating Contact Setting",
          text: "The setting may not have saved. Try again, maybe after restarting the app.",
        },
        5000,
      );
    }
    this.showGiveNumbers = newShowValue;
    if (
      newShowValue &&
      Object.keys(this.givenByMeDescriptions).length === 0 &&
      Object.keys(this.givenByMeConfirmed).length === 0 &&
      Object.keys(this.givenByMeUnconfirmed).length === 0 &&
      Object.keys(this.givenToMeDescriptions).length === 0 &&
      Object.keys(this.givenToMeConfirmed).length === 0 &&
      Object.keys(this.givenToMeUnconfirmed).length === 0
    ) {
      // assume we should load it all
      this.loadGives();
    }
  }
  private toggleShowGiveTotals() {
    if (this.showGiveTotals) {
      this.showGiveTotals = false;
      this.showGiveConfirmed = true;
    } else if (this.showGiveConfirmed) {
      this.showGiveTotals = false; // stays the same
      this.showGiveConfirmed = false;
    } else {
      this.showGiveTotals = true;
      this.showGiveConfirmed = true;
    }
  }

  private showGiveAmountsClassNames() {
    return {
      "from-slate-400": this.showGiveTotals,
      "to-slate-700": this.showGiveTotals,
      "from-green-400": !this.showGiveTotals && this.showGiveConfirmed,
      "to-green-700": !this.showGiveTotals && this.showGiveConfirmed,
      "from-yellow-400": !this.showGiveTotals && !this.showGiveConfirmed,
      "to-yellow-700": !this.showGiveTotals && !this.showGiveConfirmed,
    };
  }

  private async copySelectedContacts() {
    if (this.contactsSelected.length === 0) {
      this.danger("You must select contacts to copy.");
      return;
    }
    const selectedContactsFull = this.contacts.filter((c) =>
      this.contactsSelected.includes(c.did),
    );
    const selectedContacts: Array<Contact> = selectedContactsFull.map((c) => {
      const contact: Contact = {
        did: c.did,
        name: c.name,
      };
      if (c.nextPubKeyHashB64) {
        contact.nextPubKeyHashB64 = c.nextPubKeyHashB64;
      }
      if (c.profileImageUrl) {
        contact.profileImageUrl = c.profileImageUrl;
      }
      if (c.publicKeyBase64) {
        contact.publicKeyBase64 = c.publicKeyBase64;
      }
      return contact;
    });
    const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
      contacts: selectedContacts,
    });
    const contactsJwtUrl =
      APP_SERVER + "/deep-link/contact-import/" + contactsJwt;
    useClipboard()
      .copy(contactsJwtUrl)
      .then(() => {
        this.$notify(
          {
            group: "alert",
            type: "info",
            title: "Copied",
            text: "The link for those contacts is now in the clipboard.",
          },
          3000,
        );
      });
  }

  private showCopySelectionsInfo() {
    this.$notify(
      {
        group: "alert",
        type: "info",
        title: "Copying Contacts",
        text: "Contact info will include name, ID, profile image, and public key.",
      },
      5000,
    );
  }

  private async showOnboardMeetingDialog() {
    try {
      // First check if they're in a meeting
      const headers = await getHeaders(this.activeDid);
      const memberResponse = await this.axios.get(
        this.apiServer + "/api/partner/groupOnboardMember",
        { headers },
      );

      if (memberResponse.data.data) {
        // They're in a meeting, check if they're the host
        const hostResponse = await this.axios.get(
          this.apiServer + "/api/partner/groupOnboard",
          { headers },
        );

        if (hostResponse.data.data) {
          // They're the host, take them to setup
          this.$router.push({ name: "onboard-meeting-setup" });
        } else {
          // They're not the host, take them to list
          this.$router.push({ name: "onboard-meeting-list" });
        }
      } else {
        // They're not in a meeting, show the dialog
        this.$notify(
          {
            group: "modal",
            type: "confirm",
            title: "Onboarding Meeting",
            text: "Would you like to start a new meeting?",
            onYes: async () => {
              this.$router.push({ name: "onboard-meeting-setup" });
            },
            yesText: "Start New Meeting",
            onNo: async () => {
              this.$router.push({ name: "onboard-meeting-list" });
            },
            noText: "Join Existing Meeting",
          },
          -1,
        );
      }
    } catch (error) {
      logConsoleAndDb(
        "Error checking meeting status:" + errorStringForLog(error),
      );
      this.danger(
        "There was an error checking your meeting status.",
        "Meeting Error",
      );
    }
  }

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