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

    <!-- Heading -->
    <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
      Contact Import
    </h1>

    <div v-if="checkingImports" class="text-center">
      <fa icon="spinner" class="animate-spin" />
    </div>
    <div v-else>
      <span
        v-if="contactsImporting.length > sameCount"
        class="flex justify-center"
      >
        <input type="checkbox" v-model="makeVisible" class="mr-2" />
        Make my activity visible to these contacts.
      </span>

      <div v-if="sameCount > 0">
        <span v-if="sameCount == 1"
          >One contact is the same as an existing contact</span
        >
        <span v-else
          >{{ sameCount }} contacts are the same as existing contacts</span
        >
      </div>

      <!-- Results List -->
      <ul
        v-if="contactsImporting.length > sameCount"
        class="border-t border-slate-300"
      >
        <li v-for="(contact, index) in contactsImporting" :key="contact.did">
          <div
            v-if="
              !contactsExisting[contact.did] ||
              !R.isEmpty(contactDifferences[contact.did])
            "
            class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
          >
            <h2 class="text-base font-semibold">
              <input type="checkbox" v-model="contactsSelected[index]" />
              {{ contact.name || AppString.NO_CONTACT_NAME }}
              -
              <span v-if="contactsExisting[contact.did]" class="text-orange-500"
                >Existing</span
              >
              <span v-else class="text-green-500">New</span>
            </h2>
            <div class="text-sm truncate">
              {{ contact.did }}
            </div>
            <div v-if="contactDifferences[contact.did]">
              <div>
                <div class="grid grid-cols-3 gap-2">
                  <div></div>
                  <div class="font-bold">Old Value</div>
                  <div class="font-bold">New Value</div>
                </div>
                <div
                  v-for="(value, contactField) in contactDifferences[
                    contact.did
                  ]"
                  :key="contactField"
                  class="grid grid-cols-3 border"
                >
                  <div class="border font-bold p-1">
                    {{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
                  </div>
                  <div class="border p-1">{{ value.old }}</div>
                  <div class="border p-1">{{ value.new }}</div>
                </div>
              </div>
            </div>
          </div>
        </li>
        <button
          class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-sm text-white mt-2 px-2 py-1.5 rounded"
          @click="importContacts"
        >
          Import Selected Contacts
        </button>
      </ul>
      <p v-else-if="contactsImporting.length > 0">
        All those contacts are already in your list with the same information.
      </p>
      <div v-else>
        There are no contacts in that import. If some were sent, try again to
        get the full text and paste it. (Note that iOS cuts off data in text
        messages.) Ask the person to send the data a different way, eg. email.
        <div class="mt-4 text-center">
          <textarea
            v-model="inputJwt"
            placeholder="Contact-import data"
            class="mt-4 border-2 border-gray-300 p-2 rounded"
            cols="30"
            @input="() => checkContactJwt(inputJwt)"
          />
          <br />
          <button
            @click="() => processContactJwt(inputJwt)"
            class="ml-2 p-2 bg-blue-500 text-white rounded"
          >
            Check Import
          </button>
        </div>
      </div>
    </div>
  </section>
</template>

<script lang="ts">
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";

import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue";
import OfferDialog from "../components/OfferDialog.vue";
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
import {
  db,
  logConsoleAndDb,
  retrieveSettingsForActiveAccount,
} from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
import * as libsUtil from "../libs/util";
import {
  capitalizeAndInsertSpacesBeforeCaps,
  errorStringForLog,
  setVisibilityUtil,
} from "../libs/endorserServer";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";

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

  AppString = AppString;
  capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
  libsUtil = libsUtil;
  R = R;

  activeDid = "";
  apiServer = "";
  contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID
  contactsImporting: Array<Contact> = []; // contacts from the import
  contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
  contactDifferences: Record<
    string,
    Record<
      string,
      {
        new: string | boolean | Array<ContactMethod> | undefined;
        old: string | boolean | Array<ContactMethod> | undefined;
      }
    >
  > = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
  checkingImports = false;
  inputJwt: string = "";
  makeVisible = true;
  sameCount = 0;

  async created() {
    const settings = await retrieveSettingsForActiveAccount();
    this.activeDid = settings.activeDid || "";
    this.apiServer = settings.apiServer || "";

    // look for any imported contact array from the query parameter
    const importedContacts = (this.$route as RouteLocationNormalizedLoaded)
      .query["contacts"] as string;
    if (importedContacts) {
      await this.setContactsSelected(JSON.parse(importedContacts));
    }

    // look for a JWT after /contact-import/ in the window.location.pathname
    const jwt = window.location.pathname.match(
      /\/contact-import\/(ey.+)$/,
    )?.[1];
    if (jwt) {
      // would prefer to validate but we've got an error with JWTs on QR codes generated in the future
      // eslint-disable-next-line prettier/prettier
      // const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt);
      // decode the JWT
      const parsedJwt = decodeEndorserJwt(jwt);

      const contacts: Array<Contact> =
        parsedJwt.payload.contacts || // someday this will be the only payload sent to this page
        (Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);
      if (!contacts && parsedJwt.payload.own) {
        // handle this single-contact JWT in the contacts page, better suited to single additions
        (this.$router as Router).push({
          name: "contacts",
          query: { contactJwt: jwt },
        });
      }
      if (contacts) {
        await this.setContactsSelected(contacts);
      } else {
        // no contacts found so default message should be OK
      }
    }

    if (
      this.contactsImporting.length === 1 &&
      R.isEmpty(this.contactsExisting)
    ) {
      // if there is only one contact and it's new, then we will automatically import it
      this.contactsSelected[0] = true;
      this.importContacts(); // ... which routes to the contacts list
    }
  }

  async setContactsSelected(contacts: Array<Contact>) {
    this.contactsImporting = contacts;
    this.contactsSelected = new Array(this.contactsImporting.length).fill(true);

    await db.open();
    const baseContacts = await db.contacts.toArray();
    // set the existing contacts, keyed by DID, if they exist in contactsImporting
    for (let i = 0; i < this.contactsImporting.length; i++) {
      const contactIn = this.contactsImporting[i];
      const existingContact = baseContacts.find(
        (contact) => contact.did === contactIn.did,
      );
      if (existingContact) {
        this.contactsExisting[contactIn.did] = existingContact;

        const differences: Record<
          string,
          {
            new: string | boolean | Array<ContactMethod> | undefined;
            old: string | boolean | Array<ContactMethod> | undefined;
          }
        > = {};
        Object.keys(contactIn).forEach((key) => {
          // eslint-disable-next-line prettier/prettier
          if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) {
            differences[key] = {
              old: existingContact[key as keyof Contact],
              new: contactIn[key as keyof Contact],
            };
          }
        });
        this.contactDifferences[contactIn.did] = differences;
        if (R.isEmpty(differences)) {
          this.sameCount++;
        }

        // don't automatically import previous data
        this.contactsSelected[i] = false;
      }
    }
  }

  // check the contact-import JWT
  async checkContactJwt(jwtInput: string) {
    if (
      jwtInput.endsWith(APP_SERVER) ||
      jwtInput.endsWith(APP_SERVER + "/") ||
      jwtInput.endsWith("contact-import") ||
      jwtInput.endsWith("contact-import/")
    ) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "That is only part of the contact-import data; it's missing data at the end. Try another way to get the full data.",
        },
        5000,
      );
    }
  }

  // process the invite JWT and/or text message containing the URL with the JWT
  async processContactJwt(jwtInput: string) {
    this.checkingImports = true;

    try {
      // (For another approach used with invites, see InviteOneAcceptView.processInvite)
      const jwt: string = getContactJwtFromJwtUrl(jwtInput);
      // JWT format: { header, payload, signature, data }
      const payload = decodeEndorserJwt(jwt).payload;

      if (Array.isArray(payload.contacts)) {
        await this.setContactsSelected(payload.contacts);
      } else {
        throw new Error("Invalid contact-import JWT or URL: " + jwtInput);
      }
    } catch (error) {
      const fullError = "Error importing contacts: " + errorStringForLog(error);
      logConsoleAndDb(fullError, true);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "There was an error processing the contact-import data.",
        },
        3000,
      );
    }
    this.checkingImports = false;
  }

  async importContacts() {
    this.checkingImports = true;
    let importedCount = 0,
      updatedCount = 0;
    for (let i = 0; i < this.contactsImporting.length; i++) {
      if (this.contactsSelected[i]) {
        const contact = this.contactsImporting[i];
        const existingContact = this.contactsExisting[contact.did];
        if (existingContact) {
          await db.contacts.update(contact.did, contact);
          updatedCount++;
        } else {
          // without explicit clone on the Proxy, we get: DataCloneError: Failed to execute 'add' on 'IDBObjectStore': #<Object> could not be cloned.
          // DataError: Failed to execute 'add' on 'IDBObjectStore': Evaluating the object store's key path yielded a value that is not a valid key.
          await db.contacts.add(R.clone(contact));
          importedCount++;
        }
      }
    }
    if (this.makeVisible) {
      const failedVisibileToContacts = [];
      for (let i = 0; i < this.contactsImporting.length; i++) {
        if (this.contactsSelected[i]) {
          const contact = this.contactsImporting[i];
          if (contact) {
            const visResult = await setVisibilityUtil(
              this.activeDid,
              this.apiServer,
              this.axios,
              db,
              contact,
              true,
            );
            if (!visResult.success) {
              failedVisibileToContacts.push(contact);
            }
          }
        }
      }
      if (failedVisibileToContacts.length > 0) {
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Visibility Error",
            text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${
              failedVisibileToContacts.length == 1 ? "" : "s"
            }. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
          },
          -1,
        );
      }
    }

    this.checkingImports = false;

    this.$notify(
      {
        group: "alert",
        type: "success",
        title: "Imported",
        text:
          `${importedCount} contact${importedCount == 1 ? "" : "s"} imported.` +
          (updatedCount ? ` ${updatedCount} updated.` : ""),
      },
      3000,
    );
    (this.$router as Router).push({ name: "contacts" });
  }
}
</script>
