<template>
  <QuickNav selected="Contacts"></QuickNav>
  <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-8">
      Your Contacts
    </h1>

    <div class="flex justify-between py-2">
      <span />
      <span>
        <a
          href="/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">
      <router-link
        :to="{ name: 'contact-qr' }"
        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"
      >
        <fa icon="qrcode" class="fa-fw text-2xl" />
      </router-link>
      <textarea
        type="text"
        placeholder="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"
        v-model="contactInput"
      />
      <button
        class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
        @click="onClickNewContact()"
      >
        <fa icon="plus" class="fa-fw"></fa>
      </button>
    </div>

    <div class="w-full text-right">
      <button
        href=""
        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-1 py-1 rounded-md"
        @click="toggleShowContactAmounts()"
      >
        {{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
      </button>
    </div>
    <div class="flex justify-between mt-1" v-if="showGiveNumbers">
      <div class="w-full text-right">
        In the following, only the most recent hours are included. 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-1 rounded-md"
        >
          <fa icon="file-lines" class="fa-fw" />
        </span>
        <br />
        <button
          href=""
          class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
          v-bind:class="showGiveAmountsClassNames()"
          @click="toggleShowGiveTotals()"
        >
          {{
            showGiveTotals
              ? "Totals"
              : showGiveConfirmed
                ? "Confirmed Amounts"
                : "Unconfirmed Amounts"
          }}
          <fa icon="left-right" class="fa-fw" />
        </button>
      </div>
    </div>

    <!-- Results List -->
    <ul
      id="listContacts"
      v-if="contacts.length > 0"
      class="border-t border-slate-300"
    >
      <li
        class="border-b border-slate-300 pt-2.5 pb-4"
        v-for="contact in contacts"
        :key="contact.did"
      >
        <div class="grow overflow-hidden">
          <h2 class="text-base font-semibold">
            <EntityIcon
              :contact="contact"
              :iconSize="24"
              class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
              @click="showLargeIdenticon = contact"
            />
            {{ contact.name || AppString.NO_CONTACT_NAME }}
            <button
              @click="
                contactEdit = contact;
                contactNewName = contact.name || '';
              "
              title="Edit"
            >
              <fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1"></fa>
            </button>
            <router-link
              :to="{
                path: '/did/' + encodeURIComponent(contact.did),
              }"
              title="See more about this DID"
            >
              <fa icon="circle-info" class="text-blue-500 ml-4" />
            </router-link>
          </h2>
          <div class="text-sm truncate">
            Identifier:
            <button
              @click="
                libsUtil.doCopyTwoSecRedo(
                  contact.did,
                  () => (showDidCopy = !showDidCopy),
                )
              "
            >
              <fa icon="copy" class="text-slate-400 fa-fw"></fa>
            </button>
            <span v-show="showDidCopy" class="text-green-500">Copied DID</span>
            {{ contact.did }}
          </div>
          <div class="text-sm truncate" v-if="contact.publicKeyBase64">
            Public Key (base 64):
            <button
              @click="
                libsUtil.doCopyTwoSecRedo(
                  contact.publicKeyBase64,
                  () => (showPubKeyCopy = !showPubKeyCopy),
                )
              "
            >
              <fa icon="copy" class="text-slate-400 fa-fw"></fa>
            </button>
            <span v-show="showPubKeyCopy" class="text-green-500"
              >Copied Key</span
            >
            {{ contact.publicKeyBase64 }}
          </div>
          <div class="text-sm truncate" v-if="contact.nextPubKeyHashB64">
            Next Public Key Hash (base 64):
            <button
              @click="
                libsUtil.doCopyTwoSecRedo(
                  contact.nextPubKeyHashB64,
                  () => (showPubKeyHashCopy = !showPubKeyHashCopy),
                )
              "
            >
              <fa icon="copy" class="text-slate-400 fa-fw"></fa>
            </button>
            <span v-show="showPubKeyHashCopy" class="text-green-500"
              >Copied Hash</span
            >
            {{ contact.nextPubKeyHashB64 }}
          </div>

          <div id="ContactActions" class="flex gap-1.5 mt-2">
            <div v-if="activeDid">
              <button
                v-if="contact.seesMe && contact.did !== activeDid"
                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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
                @click="confirmSetVisibility(contact, false)"
                title="They can see you"
              >
                <fa icon="eye" class="fa-fw" />
              </button>
              <button
                v-else-if="!contact.seesMe && contact.did !== activeDid"
                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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
                @click="confirmSetVisibility(contact, true)"
                title="They cannot see you"
              >
                <fa icon="eye-slash" class="fa-fw" />
              </button>
              <!-- otherwise it's this user so hide it -->
              <fa v-else icon="eye" class="text-white mx-2.5" />

              <button
                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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
                @click="checkVisibility(contact)"
                title="Check Visibility"
                v-if="contact.did !== activeDid"
              >
                <fa icon="rotate" class="fa-fw" />
              </button>
              <!-- otherwise it's this user so hide it -->
              <fa v-else icon="rotate" class="text-white mx-2.5" />

              <button
                @click="confirmRegister(contact)"
                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 ml-6 px-2 py-1.5 rounded-md"
                v-if="contact.did !== activeDid"
                title="Registration"
              >
                <fa
                  v-if="contact.registered"
                  icon="person-circle-check"
                  class="fa-fw"
                />
                <fa v-else icon="person-circle-question" class="fa-fw" />
              </button>
              <!-- otherwise it's this user so hide it -->
              <fa v-else icon="rotate" class="text-white ml-6 px-2.5" />
            </div>

            <button
              @click="confirmDeleteContact(contact)"
              class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 px-2 py-1.5 rounded-md"
              title="Delete"
            >
              <fa icon="trash-can" class="fa-fw" />
            </button>

            <div
              v-if="showGiveNumbers && contact.did != activeDid"
              class="ml-auto flex gap-1.5"
            >
              <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-l-md"
                @click="confirmShowGiftedDialog(activeDid, contact.did)"
                :title="givenByMeDescriptions[contact.did] || ''"
              >
                To:
                <br />
                {{
                  /* eslint-disable prettier/prettier */
                  this.showGiveTotals
                    ? ((givenByMeConfirmed[contact.did] || 0)
                      + (givenByMeUnconfirmed[contact.did] || 0))
                    : this.showGiveConfirmed
                        ? (givenByMeConfirmed[contact.did] || 0)
                        : (givenByMeUnconfirmed[contact.did] || 0)
                  /* eslint-enable prettier/prettier */
                }}
                <br />
                <fa icon="plus" />
              </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 -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
                @click="confirmShowGiftedDialog(contact.did, this.activeDid)"
                :title="givenToMeDescriptions[contact.did] || ''"
              >
                From:
                <br />
                {{
                  /* eslint-disable prettier/prettier */
                  this.showGiveTotals
                    ? ((givenToMeConfirmed[contact.did] || 0)
                        + (givenToMeUnconfirmed[contact.did] || 0))
                    : this.showGiveConfirmed
                        ? (givenToMeConfirmed[contact.did] || 0)
                        : (givenToMeUnconfirmed[contact.did] || 0)
                  /* eslint-enable prettier/prettier */
                }}
                <br />
                <fa icon="plus" />
              </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 py-1.5 rounded-md border border-blue-400"
                @click="openOfferDialog(contact.did)"
              >
                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 border border-slate-400"
                title="See more given activity"
              >
                <fa icon="file-lines" class="fa-fw" />
              </router-link>
            </div>
          </div>
        </div>
      </li>
    </ul>
    <p v-else>There are no contacts.</p>

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

    <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"
          :iconSize="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>

    <div v-if="contactEdit !== null" class="dialog-overlay">
      <div class="dialog">
        <h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
        <input
          type="text"
          class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
          placeholder="Name"
          v-model="contactNewName"
        />
        <div class="flex justify-between">
          <button
            class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
            @click="onClickSaveName(contactEdit, contactNewName)"
          >
            <fa icon="save" />
          </button>
          <span class="inline-block w-2" />
          <button
            class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
            @click="onClickCancelName()"
          >
            <fa icon="ban" />
          </button>
        </div>
      </div>
    </div>
  </section>
</template>

<script lang="ts">
import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import { IndexableType } from "dexie";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";

import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
import {
  CONTACT_CSV_HEADER,
  CONTACT_URL_PREFIX,
  GiverReceiverInputInfo,
  GiveSummaryRecord,
  getHeaders,
  isDid,
  register,
  setVisibilityUtil,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue";

@Component({
  components: { GiftedDialog, EntityIcon, OfferDialog, QuickNav },
})
export default class ContactsView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;

  activeDid = "";
  apiServer = "";
  contacts: Array<Contact> = [];
  contactInput = "";
  contactEdit: Contact | null = null;
  contactNewName = "";
  // { "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;

  AppString = AppString;
  libsUtil = libsUtil;

  async created() {
    await db.open();
    const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
    this.activeDid = settings?.activeDid || "";
    this.apiServer = settings?.apiServer || "";
    this.isRegistered = !!settings?.isRegistered;

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

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

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

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

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

    try {
      const headers = await getHeaders(this.activeDid);
      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) {
      console.error("Error loading gives", error);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Load Error",
          text: "Got an error loading your gives.",
        },
        5000,
      );
    }
  }

  async onClickNewContact(): Promise<void> {
    if (!this.contactInput) {
      this.danger("There was no contact info to add.", "No Contact");
      return;
    }

    if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
      await this.addContactFromScan(this.contactInput);
      return;
    }

    if (this.contactInput.startsWith(CONTACT_CSV_HEADER)) {
      const lines = this.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) {
        this.danger("An error occurred. Some contacts may have been added.");
      }

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

    let did = this.contactInput;
    let name, publicKeyInput, nextPublicKeyHashInput;
    const commaPos1 = this.contactInput.indexOf(",");
    if (commaPos1 > -1) {
      did = this.contactInput.substring(0, commaPos1).trim();
      name = this.contactInput.substring(commaPos1 + 1).trim();
      const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
      if (commaPos2 > -1) {
        name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
        publicKeyInput = this.contactInput.substring(commaPos2 + 1).trim();
        const commaPos3 = this.contactInput.indexOf(",", commaPos2 + 1);
        if (commaPos3 > -1) {
          publicKeyInput = this.contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
          nextPublicKeyHashInput = this.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);
  }

  async addContactFromEndorserMobileLine(line: string): Promise<IndexableType> {
    // Note that Endorser Mobile puts name first, then did, etc.
    let name = line;
    let did = "";
    let publicKeyInput, seesMe, registered;
    const commaPos1 = line.indexOf(",");
    if (commaPos1 > -1) {
      name = line.substring(0, commaPos1).trim();
      did = line.substring(commaPos1 + 1).trim();
      const commaPos2 = line.indexOf(",", commaPos1 + 1);
      if (commaPos2 > -1) {
        did = line.substring(commaPos1 + 1, commaPos2).trim();
        publicKeyInput = line.substring(commaPos2 + 1).trim();
        const commaPos3 = line.indexOf(",", commaPos2 + 1);
        if (commaPos3 > -1) {
          publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
          seesMe = line.substring(commaPos3 + 1).trim() == "true";
          const commaPos4 = line.indexOf(",", commaPos3 + 1);
          if (commaPos4 > -1) {
            seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
            registered = line.substring(commaPos4 + 1).trim() == "true";
          }
        }
      }
    }
    // 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");
    }
    const newContact = {
      did,
      name,
      publicKeyBase64,
      seesMe,
      registered,
    };
    return db.contacts.add(newContact);
  }

  async addContactFromScan(url: string): Promise<void> {
    const payload = getContactPayloadFromJwtUrl(url);
    if (!payload) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "No Contact Info",
          text: "The contact info could not be parsed.",
        },
        3000,
      );
      return;
    } else {
      return this.addContact({
        did: payload.iss,
        name: payload.own.name,
        nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
        profileImageUrl: payload.own.profileImageUrl,
        publicKeyBase64: payload.own.publicEncKey,
        registered: payload.own.registered,
      } as Contact);
    }
  }

  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;
    }
    return db.contacts
      .add(newContact)
      .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 db.settings.update(MASTER_SETTINGS_KEY, {
                        hideRegisterPromptOnNewContact: stopAsking,
                      });
                      this.hideRegisterPromptOnNewContact = stopAsking;
                    }
                  },
                  onNo: async (stopAsking: boolean) => {
                    if (stopAsking) {
                      await db.settings.update(MASTER_SETTINGS_KEY, {
                        hideRegisterPromptOnNewContact: stopAsking,
                      });
                      this.hideRegisterPromptOnNewContact = stopAsking;
                    }
                  },
                  onYes: async () => {
                    await this.register(newContact);
                  },
                  promptToStopAsking: true,
                },
                -1,
              );
            }, 500);
          }
        }
        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Contact Added",
            text: addedMessage,
          },
          3000,
        );
      })
      .catch((err) => {
        console.error("Error when adding contact to storage:", err);
        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", -1);
      });
  }

  // prompt with confirmation if they want to delete a contact
  confirmDeleteContact(contact: Contact) {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Delete",
        text:
          "Are you sure you want to remove " +
          this.nameForDid(this.contacts, contact.did) +
          " with DID " +
          contact.did +
          " from your contact list?",
        onYes: async () => {
          await this.deleteContact(contact);
        },
      },
      -1,
    );
  }

  async deleteContact(contact: Contact) {
    await db.open();
    await db.contacts.delete(contact.did);
    this.contacts = R.without([contact], this.contacts);
  }

  // confirm to register a new contact
  async confirmRegister(contact: Contact) {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Register",
        text:
          "Are you sure you want to register " +
          this.nameForDid(this.contacts, contact.did) +
          (contact.registered
            ? " -- especially since they are already marked as registered"
            : "") +
          "?",
        onYes: async () => {
          await this.register(contact);
        },
      },
      -1,
    );
  }

  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;
        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.",
          },
          5000,
        );
      } else {
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Registration Error",
            text:
              (regResult.error as string) ||
              "Something went wrong during registration.",
          },
          5000,
        );
      }
    } catch (error) {
      console.error("Error when registering:", error);
      let userMessage = "There was an error. See logs for more info.";
      const serverError = error as AxiosError;
      if (serverError) {
        if (serverError.response?.data?.error?.message) {
          userMessage = serverError.response.data.error.message;
        } 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,
        },
        5000,
      );
    }
  }

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

  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?
      //console.log("Set result & seesMe", result, contact.seesMe, contact.did);
      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 {
      console.error("Got strange result from setting visibility:", 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;
    }
  }

  async checkVisibility(contact: Contact) {
    const url =
      this.apiServer +
      "/api/report/canDidExplicitlySeeMe?did=" +
      encodeURIComponent(contact.did);
    const headers = await getHeaders(this.activeDid);
    if (!headers["Authorization"]) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "No Identity",
          text: "There is no identity to use to check visibility.",
        },
        3000,
      );
      return;
    }

    try {
      const resp = await this.axios.get(url, { headers });
      if (resp.status === 200) {
        const visibility = resp.data;
        contact.seesMe = visibility;
        //console.log("Visi check:", visibility, contact.seesMe, contact.did);
        await db.contacts.update(contact.did, { seesMe: visibility });

        this.$notify(
          {
            group: "alert",
            type: "info",
            title: "Visibility Refreshed",
            text:
              this.nameForContact(contact, true) +
              " can " +
              (visibility ? "" : "not ") +
              "see your activity.",
          },
          3000,
        );
      } else {
        console.error("Got bad server response checking visibility:", resp);
        const message = resp.data.error?.message || "Got bad server response.";
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error Checking Visibility",
            text: message,
          },
          5000,
        );
      }
    } catch (err) {
      console.error("Caught error from request to check visibility:", err);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Checking Visibility",
          text: "Check connectivity and try again.",
        },
        3000,
      );
    }
  }

  private nameForDid(contacts: Array<Contact>, did: string): string {
    if (did === this.activeDid) {
      return "you";
    }
    const contact = R.find((con) => con.did == did, contacts);
    return this.nameForContact(contact);
  }

  private nameForContact(contact?: Contact, capitalize?: boolean): string {
    return (
      (contact?.name as string) ||
      (capitalize ? "This" : "this") + " unnamed user"
    );
  }

  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 as Router).push({
              name: "contact-amounts",
              query: { contactDid: giverDid },
            });
          },
        },
        -1,
      );
    } else {
      this.showGiftedDialog(giverDid, recipientDid);
    }
  }

  private showGiftedDialog(giverDid: string, recipientDid: string) {
    let giver: GiverReceiverInputInfo, receiver: GiverReceiverInputInfo;
    if (giverDid) {
      giver = {
        did: giverDid,
        name: this.nameForDid(this.contacts, giverDid),
      };
    }
    if (recipientDid) {
      receiver = {
        did: recipientDid,
        name: this.nameForDid(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;
    } 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;
    }
    (this.$refs.customGivenDialog as GiftedDialog).open(
      giver,
      receiver,
      undefined as string,
      customTitle,
      callback,
    );
  }

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

  private async onClickCancelName() {
    this.contactEdit = null;
    this.contactNewName = "";
  }

  private async onClickSaveName(contact: Contact, newName: string) {
    contact.name = newName;
    return db.contacts
      .update(contact.did, { name: newName })
      .then(() => (this.contactEdit = null));
  }

  public async toggleShowContactAmounts() {
    const newShowValue = !this.showGiveNumbers;
    try {
      await db.open();
      await db.settings.update(MASTER_SETTINGS_KEY, {
        showContactGivesInline: newShowValue,
      });
    } catch (err) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Updating Contact Setting",
          text: "The setting may not have saved. Try again, maybe after restarting the app.",
        },
        -1,
      );
      console.error(
        "Telling user to try again after contact-amounts setting update because:",
        err,
      );
    }
    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();
    }
  }
  public 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;
    }
  }

  public 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,
    };
  }
}
</script>

<style>
.dialog-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 1.5rem;
}
.dialog {
  background-color: white;
  padding: 1rem;
  border-radius: 0.5rem;
  width: 100%;
  max-width: 500px;
}

/*
  Tooltip, generated on "title" attributes on "fa" icons
  Kudos to https://www.w3schools.com/css/css_tooltip.asp
*/
/* Tooltip container */
.tooltip {
  position: relative;
  display: inline-block;
  border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
}
/* Tooltip text */
.tooltip .tooltiptext {
  visibility: hidden;
  width: 200px;
  background-color: black;
  color: #fff;
  text-align: center;
  padding: 5px 0;
  border-radius: 6px;

  position: absolute;
  z-index: 1;
}
/* How do we share with the above so code isn't duplicated? */
.tooltip .tooltiptext-left {
  visibility: hidden;
  width: 200px;
  background-color: black;
  color: #fff;
  text-align: center;
  padding: 5px 0;
  border-radius: 6px;

  position: absolute;
  z-index: 1;

  bottom: 0%;
  right: 105%;
  margin-left: -60px;
}

/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
  visibility: visible;
}
.tooltip:hover .tooltiptext-left {
  visibility: visible;
}
</style>
