<template>
  <QuickNav selected="Contacts"></QuickNav>
  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
    <!-- Sub View Heading -->
    <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
      <h1 class="grow text-xl text-center font-semibold leading-tight">
        Contact Import
      </h1>

      <!-- Back -->
      <a
        class="order-first text-lg text-center leading-none p-1"
        @click="$router.go(-1)"
      >
        <font-awesome icon="chevron-left" class="block text-center w-[1em]" />
      </a>

      <!-- Help button -->
      <router-link
        :to="{ name: 'help' }"
        class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
      >
        <font-awesome icon="question" class="block text-center w-[1em]" />
      </router-link>
    </div>

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

      <!-- Labels Section -->
      <div
        v-if="contactsImporting"
        class="mt-4 p-4 border-2 border-slate-700 rounded"
      >
        <label class="block text-sm font-medium text-gray-700 mb-2">
          Labels for Imported Contacts
        </label>

        <!-- Apply to existing contacts checkbox -->
        <div v-if="sameCount > 0" class="mb-3 flex items-center">
          <input
            id="applyLabelsToExisting"
            v-model="applyLabelsToExisting"
            type="checkbox"
            class="mr-2"
          />
          <label
            for="applyLabelsToExisting"
            class="text-sm text-gray-700 cursor-pointer"
          >
            Apply these labels to existing contacts (contacts that are already
            in your list)
          </label>
        </div>

        <!-- Selected Labels -->
        <div class="flex flex-wrap gap-2 mb-3">
          <span
            v-for="label in selectedLabels"
            :key="label"
            class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
          >
            {{ label }}
            <button
              type="button"
              class="flex-shrink-0 ml-1.5 inline-flex text-blue-400 hover:text-blue-600 focus:outline-none"
              @click="removeSelectedLabel(label)"
            >
              <font-awesome icon="xmark" class="h-4 w-4" />
            </button>
          </span>
          <span
            v-if="selectedLabels.length === 0"
            class="text-xs text-gray-400 italic"
          >
            No labels selected
          </span>
        </div>

        <!-- Label Input -->
        <div class="flex gap-2 mb-3">
          <input
            v-model="newLabelInput"
            type="text"
            placeholder="Add or create a label..."
            class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm p-2"
            @keyup.enter="addNewLabel"
          />
          <button
            class="px-4 py-1 bg-green-500 text-white rounded-md text-sm whitespace-nowrap"
            @click="addNewLabel"
          >
            Add Label
          </button>
        </div>

        <!-- Available Labels -->
        <div v-if="availableLabels.length > 0" class="mt-3">
          <p class="text-xs font-medium text-gray-700 mb-2">
            Available labels (click to toggle):
          </p>
          <div class="flex flex-wrap gap-2">
            <button
              v-for="label in availableLabels"
              :key="label"
              class="inline-flex items-center px-3 py-1 rounded text-sm font-medium transition-colors"
              :class="
                selectedLabels.includes(label)
                  ? 'bg-blue-500 text-white hover:bg-blue-600'
                  : 'bg-gray-100 text-gray-800 hover:bg-gray-200'
              "
              @click="toggleLabel(label)"
            >
              {{ label }}
            </button>
          </div>
        </div>
      </div>

      <div v-if="sameCount > 0" class="mt-2">
        <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="mt-2 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 v-model="contactsSelected[index]" type="checkbox" />
              {{ 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 v-if="contactField === 'labels'" class="border p-1">
                    {{ value.old.join(", ") }}
                  </div>
                  <div v-else class="border p-1">{{ value.old }}</div>
                  <div v-if="contactField === 'labels'" class="border p-1">
                    {{ value.new.join(", ") }}
                  </div>
                  <div v-else 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 Contacts
        </button>
      </ul>
      <p v-else-if="contactsImporting.length > 0">
        All those contacts are already in your list with the same information.
        <button
          v-if="applyLabelsToExisting"
          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"
        >
          Apply Labels to Existing Contacts
        </button>
      </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
            class="ml-2 p-2 bg-blue-500 text-white rounded"
            @click="() => processContactJwt(inputJwt)"
          >
            Check Import
          </button>
        </div>
      </div>
    </div>
  </section>
</template>

<script lang="ts">
/**
 * @file Contact Import View Component
 * @author Matthew Raymer
 *
 * This component handles the import of contacts into the TimeSafari app.
 * It supports multiple import methods and handles duplicate detection,
 * contact validation, and visibility settings.
 *
 * Import Methods:
 * 1. Direct URL Query Parameters:
 *    Example: /contact-import?contacts=[{"did":"did:example:123","name":"Alice"}]
 *
 * 2. JWT in URL Path:
 *    Example: /contact-import/eyJhbGciOiJFUzI1NksifQ...
 *    - Supports both single and bulk imports
 *    - JWT payload can be either:
 *      a) Array format: { contacts: [{did: "...", name: "..."}, ...] }
 *      b) Single contact: { own: true, did: "...", name: "..." }
 *
 * 3. Manual JWT Input:
 *    - Accepts pasted JWT strings
 *    - Validates format and content before processing
 *
 * URL Examples:
 * ```
 * # Bulk import via query params
 * /contact-import?contacts=[
 *   {"did":"did:example:123","name":"Alice"},
 *   {"did":"did:example:456","name":"Bob"}
 * ]
 *
 * # Single contact via JWT
 * /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJvd24iOnRydWUsImRpZCI6ImRpZDpleGFtcGxlOjEyMyJ9...
 *
 * # Bulk import via JWT
 * /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJjb250YWN0cyI6W3siZGlkIjoiZGlkOmV4YW1wbGU6MTIzIn1dfQ...
 *
 * # Redirect to contacts page (single contact)
 * /contacts?contactJwt=eyJhbGciOiJFUzI1NksifQ...
 * ```
 *
 * Features:
 * - Automatic duplicate detection
 * - Field-by-field comparison for existing contacts
 * - Batch visibility settings
 * - Auto-import for single new contacts
 *
 * @see {@link Contact} for contact data structure
 * @see {@link setVisibilityUtil} for visibility management
 */

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 {
  Contact,
  ContactWithLabels,
  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";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { ContactLabel } from "@/db/tables/contactLabels";

type ContactDifferences = Record<
  string,
  {
    new:
      | string
      | boolean
      | Array<ContactMethod>
      | Array<ContactLabel>
      | undefined;
    old:
      | string
      | boolean
      | Array<ContactMethod>
      | Array<ContactLabel>
      | undefined;
  }
>;

/**
 * Contact Import View Component
 * @author Matthew Raymer
 *
 * This component handles the secure import of contacts into TimeSafari via JWT tokens.
 * It supports both single and multiple contact imports with validation and duplicate detection.
 *
 * Import Workflows:
 * 1. JWT in URL Path (/contact-import/[JWT])
 *    - Extracts JWT from path
 *    - Decodes and validates contact data
 *    - Handles both single and multiple contacts
 *
 * 2. JWT in Query Parameter (/contacts?contactJwt=[JWT])
 *    - Used for single contact redirects
 *    - Processes JWT from query parameter
 *    - Redirects to appropriate view
 *
 * JWT Payload Structure:
 * ```json
 * {
 *   "iat": 1740740453,
 *   "contacts": [{
 *     "did": "did:ethr:0x...",
 *     "name": "Optional Name",
 *     "nextPubKeyHashB64": "base64 string",
 *     "publicKeyBase64": "base64 string"
 *   }],
 *   "iss": "did:ethr:0x..."
 * }
 * ```
 *
 * Security Features:
 * - JWT validation
 * - Issuer verification
 * - Duplicate detection
 * - Contact data validation
 *
 * @component
 */
@Component({
  components: { EntityIcon, OfferDialog, QuickNav },
  mixins: [PlatformServiceMixin],
})
export default class ContactImportView extends Vue {
  /** Notification function injected by Vue */
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  /** Current route instance */
  $route!: RouteLocationNormalizedLoaded;
  /** Router instance for navigation */
  $router!: Router;

  notify!: ReturnType<typeof createNotifyHelpers>;

  // Constants
  AppString = AppString;
  capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
  libsUtil = libsUtil;
  R = R;

  // Component state
  /** Active user's DID for authentication and visibility settings */
  activeDid = "";
  /** API server URL for backend communication */
  apiServer = "";
  /** Map of existing contacts keyed by DID for duplicate detection */
  contactsExisting: Record<string, ContactWithLabels> = {};
  /** Array of contacts being imported from JWT */
  contactsImporting: Array<ContactWithLabels> = [];
  /** Selection state for each importing contact */
  contactsSelected: Array<boolean> = [];
  /** Each contact's differences between existing and importing info */
  contactDifferences: Record<string, ContactDifferences> = {};
  /** Loading state for import operations */
  checkingImports = false;
  /** JWT input for manual contact import */
  inputJwt: string = "";
  /** Visibility setting for imported contacts */
  makeVisible = true;
  /** Count of duplicate contacts found */
  sameCount = 0;

  // --- Labels ---

  /** Labels selected to be applied to imported contacts */
  selectedLabels: Array<string> = [];
  /** Input for new label creation */
  newLabelInput = "";
  /** All available labels from existing contacts */
  availableLabels: Array<string> = [];
  /** Whether to apply labels to existing contacts as well */
  applyLabelsToExisting = false;

  /**
   * Component lifecycle hook that initializes the contact import process
   *
   * This method handles three distinct import scenarios:
   * 1. Query Parameter Import:
   *    - Checks for contacts in URL query parameters
   *    - Parses JSON array of contacts if present
   *
   * 2. JWT URL Import:
   *    - Extracts JWT from URL path using regex pattern '/contact-import/(ey.+)$'
   *    - Decodes JWT without validation (supports future-dated QR codes)
   *    - Handles two JWT payload formats:
   *      a. Array format: payload.contacts or direct array
   *      b. Single contact format: redirects to contacts page with JWT
   *
   * 3. Auto-Import Logic:
   *    - Automatically imports if exactly one new contact is present
   *    - Only triggers if no existing contacts match
   *
   * @throws Will not throw but logs errors during JWT processing
   * @emits router.push when redirecting for single contact import
   */
  async created() {
    this.notify = createNotifyHelpers(this.$notify);

    await this.initializeSettings();
    await this.loadAvailableLabels();
    await this.processQueryParams();
    await this.processJwtFromPath();
    await this.handleAutoImport();
  }

  /**
   * Initializes component settings from active account
   */
  private async initializeSettings() {
    const settings = await this.$accountSettings();

    // Get activeDid from active_identity table (single source of truth)
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const activeIdentity = await (this as any).$getActiveIdentity();
    this.activeDid = activeIdentity.activeDid || "";

    this.apiServer = settings.apiServer || "";
  }

  /**
   * Loads all available labels from existing contacts
   */
  private async loadAvailableLabels() {
    try {
      this.availableLabels = await this.$getUniqueContactLabels();
    } catch (error) {
      const fullError =
        "Error loading available labels: " + errorStringForLog(error);
      this.$logAndConsole(fullError, true);
      // Don't show error to user as this is non-critical
      this.availableLabels = [];
    }
  }

  /**
   * Processes contacts from URL query parameters
   */
  private async processQueryParams() {
    const importedContacts = this.$route.query["contacts"] as string;
    if (importedContacts) {
      await this.setContactsSelected(JSON.parse(importedContacts));
    }
  }

  /**
   * Processes JWT from URL path and handles different JWT formats
   */
  private async processJwtFromPath() {
    // JWT tokens always start with 'ey' (base64url encoded header)
    const JWT_PATTERN = /\/contact-import\/(ey.+)$/;
    const jwt = window.location.pathname.match(JWT_PATTERN)?.[1];

    if (jwt) {
      const parsedJwt = decodeEndorserJwt(jwt);
      const contacts: Array<Contact> =
        parsedJwt.payload.contacts ||
        (Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);

      if (!contacts && parsedJwt.payload.own) {
        this.$router.push({
          name: "contacts",
          query: { contactJwt: jwt },
        });
        return;
      }

      if (contacts) {
        await this.setContactsSelected(contacts);
      }
    }
  }

  /**
   * Handles automatic import for single new contacts
   */
  private async handleAutoImport() {
    if (
      this.contactsImporting.length === 1 &&
      R.isEmpty(this.contactsExisting)
    ) {
      this.contactsSelected[0] = true;
      await this.importContacts();
    }
  }

  /**
   * Processes contacts for import and checks for duplicates
   * @param contacts Array of contacts to process
   */
  async setContactsSelected(contacts: Array<ContactWithLabels>) {
    this.contactsImporting = contacts;
    this.contactsSelected = new Array(this.contactsImporting.length).fill(
      false,
    );

    // Get all existing contacts for comparison
    const baseContacts = await this.$getAllContacts();

    // get the labels for each contact

    // Check for existing contacts and differences
    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) {
        const labels = await this.$getContactLabelsForDid(existingContact.did);
        this.contactsExisting[contactIn.did] = {
          ...existingContact,
          labels: labels || [],
        };
        const existingFullContact = this.contactsExisting[contactIn.did];

        // Compare contact fields for differences
        const differences: ContactDifferences = {};
        Object.keys(contactIn).forEach((key) => {
          if (
            !R.equals(
              contactIn[key as keyof Contact],
              existingFullContact[key as keyof Contact],
            )
          ) {
            differences[key] = {
              old: existingFullContact[key as keyof Contact],
              new: contactIn[key as keyof Contact],
            };
          }
        });
        this.contactDifferences[contactIn.did] = differences;
        if (R.isEmpty(differences)) {
          this.sameCount++;
        } else {
          // auto-select contacts with differences
          this.contactsSelected[i] = true;
        }
      } else {
        // auto-select new contacts
        this.contactsSelected[i] = true;
      }
    }
  }

  /**
   * Validates contact import JWT format
   * @param jwtInput JWT string to validate
   */
  async checkContactJwt(jwtInput: string) {
    if (
      jwtInput.endsWith(APP_SERVER) ||
      jwtInput.endsWith(APP_SERVER + "/") ||
      jwtInput.endsWith("contact-import") ||
      jwtInput.endsWith("contact-import/")
    ) {
      this.notify.error(
        "That is only part of the contact-import data; it's missing data at the end. Try another way to get the full data.",
        TIMEOUTS.LONG,
      );
    }
  }

  /**
   * Processes contact import JWT and updates contacts
   * @param jwtInput JWT string containing contact data
   */
  async processContactJwt(jwtInput: string) {
    this.checkingImports = true;

    try {
      const jwt: string = getContactJwtFromJwtUrl(jwtInput) || "";
      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);
      this.$logAndConsole(fullError, true);
      this.notify.error(
        "There was an error processing the contact-import data.",
        TIMEOUTS.STANDARD,
      );
    }
    this.checkingImports = false;
  }

  /**
   * Adds a new label to the selected labels list
   */
  addNewLabel() {
    const label = this.newLabelInput.trim();
    if (label && !this.selectedLabels.includes(label)) {
      this.selectedLabels.push(label);
      // Add to available labels if it's new
      if (!this.availableLabels.includes(label)) {
        this.availableLabels.push(label);
        this.availableLabels.sort();
      }
      this.newLabelInput = "";
    }
  }

  /**
   * Removes a label from the selected labels list
   * @param label Label to remove
   */
  removeSelectedLabel(label: string) {
    const index = this.selectedLabels.indexOf(label);
    if (index > -1) {
      this.selectedLabels.splice(index, 1);
    }
  }

  /**
   * Toggles a label in the selected labels list
   * @param label Label to toggle
   */
  toggleLabel(label: string) {
    const index = this.selectedLabels.indexOf(label);
    if (index > -1) {
      this.selectedLabels.splice(index, 1);
    } else {
      this.selectedLabels.push(label);
    }
  }

  /**
   * Imports selected contacts and sets visibility if requested
   * Updates existing contacts or adds new ones
   */
  async importContacts() {
    this.checkingImports = true;
    let importedCount = 0,
      updatedCount = 0;

    // Process selected contacts (new contacts or existing with changes)
    for (let i = 0; i < this.contactsImporting.length; i++) {
      if (this.contactsSelected[i]) {
        const contactWithLabels = this.contactsImporting[i];
        const contact = {
          ...contactWithLabels,
          labels: undefined,
        };
        // Merge existing labels with selected labels for imported contacts
        const existingLabels = contactWithLabels.labels || [];
        const mergedLabels = Array.from(
          new Set([...existingLabels, ...this.selectedLabels]),
        );
        const existingContact = this.contactsExisting[contact.did];

        if (existingContact) {
          // Update existing contact
          await this.$updateContact(contact.did, contact);
          // update the labels for the contact
          await this.$updateContactLabels(contact.did, mergedLabels);
          updatedCount++;
        } else {
          // Add new contact
          await this.$insertContact(contact);
          // add the labels for the contact
          await this.$insertContactLabels(contact.did, mergedLabels);
          importedCount++;
        }
      }
    }

    let labelsAppliedToExistingCount = 0;
    // Apply selected labels to existing contacts with no changes if checkbox is checked
    if (this.applyLabelsToExisting && this.selectedLabels.length > 0) {
      for (let i = 0; i < this.contactsImporting.length; i++) {
        // Skip if this contact was already processed (selected)
        if (!this.contactsSelected[i]) {
          const contactWithLabels = this.contactsImporting[i];
          const existingContact = this.contactsExisting[contactWithLabels.did];

          // Only process existing contacts (not selected means no changes)
          if (existingContact) {
            // Get current labels for this contact
            const currentLabels = await this.$getContactLabelsForDid(
              contactWithLabels.did,
            );
            // Merge current labels with selected labels
            const mergedLabels = Array.from(
              new Set([...currentLabels, ...this.selectedLabels]),
            );
            // Update labels if there are new ones to add
            if (mergedLabels.length > currentLabels.length) {
              await this.$updateContactLabels(
                contactWithLabels.did,
                mergedLabels,
              );
              labelsAppliedToExistingCount++;
            }
          }
        }
      }
    }

    // Set visibility if requested
    if (this.makeVisible) {
      const failedVisibileToContacts = [];
      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) {
            const visResult = await setVisibilityUtil(
              this.activeDid,
              this.apiServer,
              this.axios,
              contact,
              true,
            );
            if (!visResult.success) {
              failedVisibileToContacts.push(contact);
            }
          }
        }
      }
      if (failedVisibileToContacts.length > 0) {
        this.notify.error(
          `Failed to set visibility for ${failedVisibileToContacts.length} contact${
            failedVisibileToContacts.length == 1 ? "" : "s"
          }. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
          TIMEOUTS.MODAL,
        );
      }
    }

    this.checkingImports = false;

    // Show success notification
    this.notify.success(
      `${importedCount} contact${importedCount == 1 ? "" : "s"} imported.` +
        (updatedCount ? ` ${updatedCount} updated.` : "") +
        (labelsAppliedToExistingCount
          ? ` ${labelsAppliedToExistingCount} labels applied to existing contacts.`
          : ""),
      TIMEOUTS.STANDARD,
    );
    this.$router.push({ name: "contacts" });
  }
}
</script>
