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

  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
    <TopMessage />

    <!-- Main View Heading -->
    <div class="flex gap-4 items-center mb-4">
      <h1 id="ViewHeading" class="text-2xl font-bold leading-none">
        Your Contacts
      </h1>

      <!-- 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 class="flex justify-between py-2 mt-4">
      <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 -->
    <ContactInputForm
      v-model="contactInput"
      :is-registered="isRegistered"
      :on-submit="onClickNewContact"
      :on-show-onboard-meeting="showOnboardMeetingDialog"
      :on-registration-required="
        () =>
          notify.warning(
            'You must get registered before you can create invites.',
          )
      "
      :on-navigate-onboard-meeting="
        () => $router.push({ name: 'onboard-meeting-list' })
      "
      :on-update-model-value="(value: string) => (contactInput = value)"
      @qr-scan="handleQRCodeClick"
    />

    <ContactListHeader
      v-if="contacts.length > 0"
      :show-give-numbers="showGiveNumbers"
      :all-contacts-selected="allContactsSelected"
      :copy-button-class="copyButtonClass"
      :copy-button-disabled="copyButtonDisabled"
      :give-amounts-button-text="giveAmountsButtonText"
      :show-actions-button-text="showActionsButtonText"
      :give-amounts-button-class="showGiveAmountsClassNames()"
      @toggle-all-selection="toggleAllContactsSelection"
      @copy-selected="copySelectedContacts"
      @show-copy-info="showCopySelectionsInfo"
      @toggle-give-totals="toggleShowGiveTotals"
      @toggle-show-actions="toggleShowContactAmounts"
    />
    <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>

    <!-- Label Filter -->
    <div v-if="allLabels.length > 0" class="mt-4 mb-2">
      <div class="flex items-center justify-between pl-[16.666%] pr-[16.666%]">
        <button
          class="text-sm font-medium text-blue-600 flex items-center gap-1"
          @click="showLabelFilter = !showLabelFilter"
        >
          <font-awesome
            :icon="showLabelFilter ? 'chevron-up' : 'filter'"
            class="text-xs"
          />
          {{ showLabelFilter ? "Hide Filters" : "Filter by Labels" }}
          <span
            v-if="selectedLabels.length > 0"
            class="bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-[10px]"
          >
            {{ selectedLabels.length }}
          </span>
        </button>
        <button
          :class="[
            'text-sm text-red-500 underline',
            selectedLabels.length === 0 ? 'invisible' : '',
          ]"
          @click="clearLabelFilters"
        >
          Clear all
        </button>
      </div>
      <div
        v-if="showLabelFilter"
        class="flex flex-wrap gap-2 mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200"
      >
        <button
          v-for="label in allLabels"
          :key="label"
          class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors"
          :class="
            selectedLabels.includes(label)
              ? 'bg-blue-600 text-white border-blue-600'
              : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'
          "
          @click="toggleLabel(label)"
        >
          {{ label }}
        </button>
      </div>
    </div>

    <!-- Results List -->
    <ul
      v-if="contacts.length > 0"
      id="listContacts"
      class="border-t border-slate-300 my-2"
    >
      <ContactListItem
        v-for="contact in contactsFiltered"
        :key="contact.did"
        :contact="contact"
        :active-did="activeDid"
        :show-checkbox="!showGiveNumbers"
        :show-actions="showGiveNumbers"
        :is-selected="contactsSelected.includes(contact.did)"
        :show-give-totals="showGiveTotals"
        :show-give-confirmed="showGiveConfirmed"
        :given-to-me-descriptions="givenToMeDescriptions"
        :given-to-me-confirmed="givenToMeConfirmed"
        :given-to-me-unconfirmed="givenToMeUnconfirmed"
        :given-by-me-descriptions="givenByMeDescriptions"
        :given-by-me-confirmed="givenByMeConfirmed"
        :given-by-me-unconfirmed="givenByMeUnconfirmed"
        @toggle-selection="toggleContactSelection"
        @show-identicon="showLargeIdenticon = $event"
        @show-gifted-dialog="confirmShowGiftedDialog"
        @open-offer-dialog="openOfferDialog"
      />
    </ul>
    <p v-else>There are no contacts.</p>

    <ContactBulkActions
      v-if="contacts.length > 0"
      :show-give-numbers="showGiveNumbers"
      :all-contacts-selected="allContactsSelected"
      :copy-button-class="copyButtonClass"
      :copy-button-disabled="copyButtonDisabled"
      @toggle-all-selection="toggleAllContactsSelection"
      @copy-selected="copySelectedContacts"
    />

    <GiftedDialog
      ref="customGivenDialog"
      :initial-giver-entity-type="'person'"
      :initial-recipient-entity-type="'person'"
    />
    <OfferDialog ref="customOfferDialog" />
    <ContactNameDialog ref="contactNameDialog" />

    <LargeIdenticonModal
      :contact="showLargeIdenticon"
      @close="showLargeIdenticon = undefined"
    />
  </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 QuickNav from "../components/QuickNav.vue";
import { copyToClipboard } from "../services/ClipboardService";
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 ContactListItem from "../components/ContactListItem.vue";
import ContactInputForm from "../components/ContactInputForm.vue";
import ContactListHeader from "../components/ContactListHeader.vue";
import ContactBulkActions from "../components/ContactBulkActions.vue";
import LargeIdenticonModal from "../components/LargeIdenticonModal.vue";
import { AppString, NotificationIface } from "../constants/app";
// Legacy logging import removed - using PlatformServiceMixin methods
import { Contact } from "../db/tables/contacts";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";
import {
  CONTACT_CSV_HEADER,
  createEndorserJwtForDid,
  errorStringForLog,
  getHeaders,
  isDid,
  register,
  setVisibilityUtil,
  CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
  CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
  CONTACT_URL_PATH_ENDORSER_CH_OLD,
} from "../libs/endorserServer";
import {
  GiveSummaryRecord,
  UserInfo,
  VerifiableCredential,
} from "@/interfaces";
import * as libsUtil from "../libs/util";
import { generateSaveAndActivateIdentity } from "../libs/util";
import { logger } from "../utils/logger";
// No longer needed - using PlatformServiceMixin methods
// import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { isDatabaseError } from "@/interfaces/common";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { APP_SERVER, DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { QRNavigationService } from "@/services/QRNavigationService";
import {
  NOTIFY_CONTACT_NO_INFO,
  NOTIFY_CONTACTS_ADD_ERROR,
  NOTIFY_CONTACT_NO_DID,
  NOTIFY_CONTACT_INVALID_DID,
  NOTIFY_CONTACTS_ADDED_VISIBLE,
  NOTIFY_CONTACTS_ADDED,
  NOTIFY_CONTACTS_ADDED_CONFIRM,
  NOTIFY_CONTACT_IMPORT_ERROR,
  NOTIFY_CONTACT_IMPORT_CONFLICT,
  NOTIFY_CONTACT_IMPORT_CONSTRAINT,
  NOTIFY_CONTACT_SETTING_SAVE_ERROR,
  NOTIFY_CONTACT_INFO_COPY,
  NOTIFY_CONTACTS_SELECT_TO_COPY,
  NOTIFY_CONTACT_LINK_COPIED,
  NOTIFY_BLANK_INVITE,
  NOTIFY_INVITE_REGISTRATION_SUCCESS,
  NOTIFY_CONTACTS_ADDED_CSV,
  NOTIFY_CONTACT_INPUT_PARSE_ERROR,
  NOTIFY_CONTACT_NO_CONTACT_FOUND,
  NOTIFY_GIVES_LOAD_ERROR,
  NOTIFY_MEETING_STATUS_ERROR,
  NOTIFY_REGISTRATION_ERROR_FALLBACK,
  NOTIFY_REGISTRATION_ERROR_GENERIC,
  NOTIFY_VISIBILITY_ERROR_FALLBACK,
  NOTIFY_EXPORT_DATA_PROMPT,
  getRegisterPersonSuccessMessage,
  getVisibilitySuccessMessage,
  getGivesRetrievalErrorMessage,
} from "@/constants/notifications";

/**
 * ContactsView - Main contact management interface
 *
 * This view provides comprehensive contact management functionality including:
 * - Contact display and filtering
 * - Contact creation from various input formats (DID, JWT, CSV, JSON)
 * - Contact selection and bulk operations
 * - Give amounts display and management
 * - Contact registration and visibility settings
 * - QR code scanning integration
 * - Meeting onboarding functionality
 *
 * The component uses the Enhanced Triple Migration Pattern with:
 * - PlatformServiceMixin for database operations
 * - Centralized notification constants
 * - Computed properties for template streamlining
 * - Refactored methods for maintainability
 *
 * @author Matthew Raymer
 */
@Component({
  name: "ContactsView",
  components: {
    GiftedDialog,
    EntityIcon,
    OfferDialog,
    QuickNav,
    ContactNameDialog,
    TopMessage,
    ContactListItem,
    ContactInputForm,
    ContactListHeader,
    ContactBulkActions,
    LargeIdenticonModal,
  },
  mixins: [PlatformServiceMixin],
})
export default class ContactsView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  $route!: RouteLocationNormalizedLoaded;
  $router!: Router;

  /** Notification helpers */
  notify!: ReturnType<typeof createNotifyHelpers>;

  activeDid = "";
  apiServer = "";
  contacts: Array<Contact> = [];
  contactsFiltered: 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;

  /** Label management state */
  selectedLabels: string[] = [];
  allLabels: string[] = [];
  showLabelFilter = false;

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

  /**
   * Component lifecycle hook - Initialize component state and load data
   * Sets up notification helpers, loads user settings, processes URL parameters,
   * and loads contacts from database
   */
  public async created() {
    this.notify = createNotifyHelpers(this.$notify);

    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 || DEFAULT_ENDORSER_API_SERVER;
    this.isRegistered = !!settings.isRegistered;

    logger.debug("[ContactsView] Created with settings:", {
      activeDid: this.activeDid,
      apiServer: this.apiServer,
      isRegistered: this.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();
    }

    this.contacts = await this.$getAllContacts();
    this.contactsFiltered = await this.filteredContacts();
    this.allLabels = await this.$getUniqueContactLabels();
  }

  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.error(NOTIFY_BLANK_INVITE.message, TIMEOUTS.VERY_LONG);
    } else if (importedInviteJwt) {
      logger.debug("[ContactsView] Processing invite JWT, current activeDid:", {
        activeDid: this.activeDid,
      });

      // Re-fetch settings after ensuring active_identity is populated
      const updatedSettings = await this.$accountSettings();
      this.activeDid = updatedSettings.activeDid || "";
      this.apiServer = updatedSettings.apiServer || DEFAULT_ENDORSER_API_SERVER;

      // Identity creation should be handled by router guard, but keep as fallback for invite processing
      if (!this.activeDid) {
        logger.info(
          "[ContactsView] No active DID found, creating identity as fallback for invite processing",
        );
        this.activeDid = await generateSaveAndActivateIdentity();
        logger.info("[ContactsView] Created new identity:", {
          activeDid: this.activeDid,
        });
      }
      // send invite directly to server, with auth for this user
      const headers = await getHeaders(this.activeDid);
      logger.debug("[ContactsView] Making API request to claim invite:", {
        apiServer: this.apiServer,
        activeDid: this.activeDid,
        hasApiServer: !!this.apiServer,
        apiServerLength: this.apiServer?.length || 0,
        fullUrl: this.apiServer + "/api/v2/claim",
      });
      try {
        const response = await this.axios.post(
          this.apiServer + "/api/v2/claim",
          { jwtEncoded: importedInviteJwt },
          { headers },
        );
        if (response.status != 201) {
          throw { error: { response: response } };
        }
        await this.$saveUserSettings(this.activeDid, { isRegistered: true });
        this.isRegistered = true;
        this.notify.success(NOTIFY_INVITE_REGISTRATION_SUCCESS.message);

        // 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;
        logger.debug(
          "[ContactsView] Opening ContactNameDialog for invite processing",
        );
        (this.$refs.contactNameDialog as ContactNameDialog).open(
          "Who Invited You?",
          "",
          async (name) => {
            await this.addContact({
              did: (
                registration.vc.credentialSubject.agent as {
                  identifier: string;
                }
              ).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 as {
                  identifier: string;
                }
              ).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();
          },
        );
      } catch (error: unknown) {
        const fullError = "Error redeeming invite: " + errorStringForLog(error);
        this.$logAndConsole(fullError, true);
        let message = "Got an error sending the invite.";
        if (
          error &&
          typeof error === "object" &&
          "response" in error &&
          error.response &&
          typeof error.response === "object" &&
          "data" in error.response &&
          error.response.data &&
          typeof error.response.data === "object" &&
          "error" in error.response.data
        ) {
          const responseData = error.response.data as { error: unknown };
          if (
            responseData.error &&
            typeof responseData.error === "object" &&
            "message" in responseData.error
          ) {
            message = (responseData.error as { message: string }).message;
          } else {
            message = String(responseData.error);
          }
        } else if (error && typeof error === "object" && "message" in error) {
          message = (error as { message: string }).message;
        }
        this.notify.error(message, TIMEOUTS.MODAL);
      }
      // if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
      this.$router.push({ path: "/contacts" });
    }
  }

  // Legacy danger() and warning() methods removed - now using this.notify.error() and this.notify.warning()

  private showOnboardingInfo() {
    this.notify.confirm(
      NOTIFY_CONTACTS_ADDED_CONFIRM.message,
      async () => {
        this.$router.push({ name: "home" });
      },
      -1,
    );
  }

  // Computed properties for template streamlining
  async filteredContacts() {
    let filtered = this.showGiveNumbers
      ? this.contactsSelected.length === 0
        ? this.contacts
        : this.contacts.filter((contact) =>
            this.contactsSelected.includes(contact.did),
          )
      : this.contacts;

    // Apply label filtering if any labels are selected
    if (this.selectedLabels.length > 0) {
      const contactIdsWithAllLabels = await this.$getContactIdsWithAllLabels(
        this.selectedLabels,
      );
      filtered = filtered.filter((contact: Contact) =>
        contactIdsWithAllLabels.includes(contact.did),
      );
    }
    return filtered;
  }

  /**
   * Toggle a label in the filter selection
   * @param label The label to toggle
   */
  async toggleLabel(label: string) {
    if (this.selectedLabels.includes(label)) {
      this.selectedLabels = this.selectedLabels.filter((l) => l !== label);
    } else {
      this.selectedLabels.push(label);
    }
    this.contactsSelected = [];
    this.contactsFiltered = await this.filteredContacts();
  }

  /**
   * Clear all active label filters
   */
  async clearLabelFilters() {
    this.selectedLabels = [];
    this.contactsSelected = [];
    this.contactsFiltered = await this.filteredContacts();
  }

  get copyButtonClass() {
    return this.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";
  }

  get copyButtonDisabled() {
    return this.contactsSelected.length === 0;
  }

  get giveAmountsButtonText() {
    if (this.showGiveTotals) {
      return "Totals";
    }
    return this.showGiveConfirmed ? "Confirmed Amounts" : "Unconfirmed Amounts";
  }

  get showActionsButtonText() {
    return this.showGiveNumbers ? "Hide Actions" : "See Actions";
  }

  get allContactsSelected() {
    return this.contactsSelected.length === this.contactsFiltered.length;
  }

  // Helper methods for template interactions
  toggleAllContactsSelection(): void {
    if (this.allContactsSelected) {
      this.contactsSelected = [];
    } else {
      this.contactsSelected = this.contactsFiltered.map(
        (contact) => contact.did,
      );
    }
  }

  toggleContactSelection(contactDid: string): void {
    if (this.contactsSelected.includes(contactDid)) {
      this.contactsSelected.splice(
        this.contactsSelected.indexOf(contactDid),
        1,
      );
    } else {
      this.contactsSelected.push(contactDid);
    }
  }

  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.error(getGivesRetrievalErrorMessage(useRecipient));
      }
    };

    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) {
      const fullError = "Error loading gives: " + errorStringForLog(error);
      this.$logAndConsole(fullError, true);
      this.notify.error(NOTIFY_GIVES_LOAD_ERROR.message);
    }
  }

  /**
   * Main method to handle new contact input processing
   * Routes to appropriate parsing method based on input format
   */
  private async onClickNewContact(): Promise<void> {
    const contactInput = this.contactInput.trim();
    if (!contactInput) {
      this.notify.error(NOTIFY_CONTACT_NO_INFO.message);
      return;
    }

    // Try different parsing methods in order
    if (await this.tryParseJwtContact(contactInput)) return;
    if (await this.tryParseCsvContacts(contactInput)) return;
    if (await this.tryParseDidContact(contactInput)) return;
    if (await this.tryParseJsonContacts(contactInput)) return;

    // If no parsing method succeeded
    this.notify.error(NOTIFY_CONTACT_NO_CONTACT_FOUND.message);
  }

  /**
   * Parse contact from JWT URL format
   */
  private async tryParseJwtContact(contactInput: string): Promise<boolean> {
    if (
      contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI) ||
      contactInput.includes(CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI) ||
      contactInput.includes(CONTACT_URL_PATH_ENDORSER_CH_OLD)
    ) {
      const jwt = getContactJwtFromJwtUrl(contactInput);
      if (jwt) {
        const { payload } = decodeEndorserJwt(jwt);
        const userInfo = payload["own"] as UserInfo;
        const newContact = {
          did: userInfo.did || payload["iss"],
          name: userInfo.name,
          nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
          profileImageUrl: userInfo.profileImageUrl,
          publicKeyBase64: userInfo.publicEncKey,
          registered: userInfo.registered,
        } as Contact;
        await this.addContact(newContact);
        return true;
      }
    }
    return false;
  }

  /**
   * Parse contacts from CSV format
   */
  private async tryParseCsvContacts(contactInput: string): Promise<boolean> {
    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.success(NOTIFY_CONTACTS_ADDED_CSV.message);
      } catch (e) {
        const fullError =
          "Error adding contacts from CSV: " + errorStringForLog(e);
        this.$logAndConsole(fullError, true);
        this.notify.error(NOTIFY_CONTACTS_ADD_ERROR.message);
      }

      this.contacts = await this.$getAllContacts();
      this.contactsFiltered = await this.filteredContacts();
      return true;
    }
    return false;
  }

  /**
   * Parse contact from DID format with optional parameters
   */
  private async tryParseDidContact(contactInput: string): Promise<boolean> {
    if (contactInput.startsWith("did:")) {
      const parsedContact = this.parseDidContactString(contactInput);
      await this.addContact(parsedContact);
      return true;
    }
    return false;
  }

  /**
   * Parse DID contact string into Contact object
   */
  private parseDidContactString(contactInput: string): Contact {
    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();
          nextPublicKeyHashInput = contactInput.substring(commaPos3 + 1).trim();
        }
      }
    }

    // Convert hex keys to base64 if needed
    const publicKeyBase64 = this.convertHexToBase64(publicKeyInput);
    const nextPubKeyHashB64 = this.convertHexToBase64(nextPublicKeyHashInput);

    return {
      did,
      name,
      publicKeyBase64,
      nextPubKeyHashB64,
    };
  }

  /**
   * Convert hex string to base64 if it matches hex pattern
   */
  private convertHexToBase64(hexString?: string): string | undefined {
    if (!hexString || !/^[0-9A-Fa-f]{66}$/i.test(hexString)) {
      return hexString;
    }
    return Buffer.from(hexString, "hex").toString("base64");
  }

  /**
   * Parse contacts from JSON array format
   */
  private async tryParseJsonContacts(contactInput: string): Promise<boolean> {
    if (contactInput.includes("[")) {
      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) },
        });
        return true;
      } catch (e) {
        const fullError =
          "Error adding contacts from array: " + errorStringForLog(e);
        this.$logAndConsole(fullError, true);
        this.notify.error(NOTIFY_CONTACT_INPUT_PARSE_ERROR.message);
      }
    }
    return false;
  }

  private async addContactFromEndorserMobileLine(
    lineRaw: string,
  ): Promise<IndexableType> {
    const newContact = libsUtil.csvLineToContact(lineRaw);
    // Replace PlatformServiceFactory with mixin method
    await this.$insertContact(newContact);
    // Return the DID as the indexable type for compatibility
    return newContact.did as IndexableType;
  }

  /**
   * Add a new contact to the database and update UI
   * Validates contact data, inserts into database, updates local state,
   * sets visibility, and handles registration prompts
   */
  private async addContact(newContact: Contact) {
    // Validate contact data
    if (!this.validateContactData(newContact)) {
      return;
    }

    try {
      // Insert contact into database
      await this.$insertContact(newContact);

      // Update local contacts list
      this.updateContactsList(newContact);
      this.contactsFiltered = await this.filteredContacts();

      // Set visibility and get success message
      const addedMessage = await this.handleContactVisibility(newContact);

      // Show success notification
      this.notify.success(addedMessage);

      // Clear input field
      this.contactInput = "";

      // Handle registration prompt if needed
      await this.handleRegistrationPrompt(newContact);
    } catch (err) {
      this.handleContactAddError(err);
    }
  }

  /**
   * Validate contact data before insertion
   */
  private validateContactData(newContact: Contact): boolean {
    if (!newContact.did) {
      this.notify.error(NOTIFY_CONTACT_NO_DID.message);
      return false;
    }
    if (!isDid(newContact.did)) {
      this.notify.error(NOTIFY_CONTACT_INVALID_DID.message);
      return false;
    }
    return true;
  }

  /**
   * Update local contacts list with new contact
   */
  private updateContactsList(newContact: Contact): void {
    const allContacts = this.contacts.concat([newContact]);
    this.contacts = R.sort(
      (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
      allContacts,
    );
  }

  /**
   * Handle contact visibility settings and return appropriate message
   */
  private async handleContactVisibility(newContact: Contact): Promise<string> {
    if (this.activeDid) {
      await this.setVisibility(newContact, true, false);
      newContact.seesMe = true;
      return NOTIFY_CONTACTS_ADDED_VISIBLE.message;
    } else {
      return NOTIFY_CONTACTS_ADDED.message;
    }
  }

  /**
   * Handle registration prompt for new contacts
   */
  private async handleRegistrationPrompt(newContact: Contact): Promise<void> {
    if (
      this.isRegistered === false || // the current Identity is not registered OR
      this.hideRegisterPromptOnNewContact === true || // the user has hidden the registrationprompt OR
      newContact.registered === true // the new contact is already registered
    ) {
      // if any of the above are true, we do not want to show the registration prompt
      await this.showExportDataPrompt();
      return;
    }

    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Register",
        text: "Do you want to register them?",
        onCancel: async (stopAsking?: boolean) => {
          await this.handleRegistrationPromptResponse(stopAsking);
          // Show export prompt after registration prompt is dismissed
          await this.showExportDataPrompt();
        },
        onNo: async (stopAsking?: boolean) => {
          await this.handleRegistrationPromptResponse(stopAsking);
          // Show export prompt after registration prompt is dismissed
          await this.showExportDataPrompt();
        },
        onYes: async () => {
          await this.register(newContact);
          // Show export prompt after registration completes
          await this.showExportDataPrompt();
        },
        promptToStopAsking: true,
      },
      -1,
    );
  }

  /**
   * Handle user response to registration prompt
   */
  private async handleRegistrationPromptResponse(
    stopAsking?: boolean,
  ): Promise<void> {
    if (stopAsking) {
      await this.$saveSettings({
        hideRegisterPromptOnNewContact: stopAsking,
      });
      this.hideRegisterPromptOnNewContact = stopAsking;
    }
  }

  /**
   * Handle errors during contact addition
   */
  private handleContactAddError(err: unknown): void {
    const fullError =
      "Error when adding contact to storage: " + errorStringForLog(err);
    this.$logAndConsole(fullError, true);

    let message = NOTIFY_CONTACT_IMPORT_ERROR.message;

    // Use type-safe error checking with our new type guards
    if (isDatabaseError(err)) {
      if (err.message.includes("Key already exists in the object store")) {
        message = NOTIFY_CONTACT_IMPORT_CONFLICT.message;
      }
      if (err.name === "ConstraintError") {
        message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message;
      }
    }

    this.notify.error(message, TIMEOUTS.LONG);
  }

  /**
   * Register a contact with the endorser server
   * Sends registration request and updates contact status on success
   * Note: This method is also used in DIDView.vue
   */
  private async register(contact: Contact) {
    this.notify.sent();

    try {
      const regResult = await register(
        this.activeDid,
        this.apiServer,
        this.axios,
        contact,
      );
      if (regResult.success) {
        contact.registered = true;
        // Replace PlatformServiceFactory with mixin method
        await this.$updateContact(contact.did, { registered: true });

        this.notify.success(getRegisterPersonSuccessMessage(contact.name));
      } else {
        this.notify.error(
          (regResult.error as string) ||
            NOTIFY_REGISTRATION_ERROR_FALLBACK.message,
          TIMEOUTS.MODAL,
        );
      }
    } catch (error) {
      const fullError = "Error when registering: " + errorStringForLog(error);
      this.$logAndConsole(fullError, true);
      let userMessage = NOTIFY_REGISTRATION_ERROR_GENERIC.message;
      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.error(userMessage, TIMEOUTS.MODAL);
    }
  }

  /**
   * Set visibility for a contact on the endorser server
   * Note: This method is also used in DIDView.vue
   */
  private async setVisibility(
    contact: Contact,
    visibility: boolean,
    showSuccessAlert: boolean,
  ) {
    const result = await setVisibilityUtil(
      this.activeDid,
      this.apiServer,
      this.axios,
      contact,
      visibility,
    );
    if (result.success) {
      //contact.seesMe = visibility; // why doesn't it affect the UI from here?
      if (showSuccessAlert) {
        this.notify.success(
          getVisibilitySuccessMessage(contact.name, visibility),
        );
      }
      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) || NOTIFY_VISIBILITY_ERROR_FALLBACK.message;
      this.notify.error(message, TIMEOUTS.LONG);
      return false;
    }
  }

  /**
   * Confirm and show gifted dialog with unconfirmed amounts check
   * If there are unconfirmed amounts, prompts user to confirm them first
   */
  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: "Confirm First?",
          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;
    // 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;
      };
    } else {
      // must be (recipientDid == this.activeDid)
      callback = (amount: number) => {
        const newList = R.clone(this.givenToMeUnconfirmed);
        newList[giverDid] = (newList[giverDid] || 0) + amount;
        this.givenToMeUnconfirmed = newList;
      };
    }
    (this.$refs.customGivenDialog as GiftedDialog).open(
      giver,
      receiver,
      undefined as unknown as string,
      undefined as unknown as string,
      undefined as unknown as string,
      undefined as unknown as string,
      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 this.$saveSettings({
        showContactGivesInline: newShowValue,
      });
    } catch (err) {
      const fullError =
        "Error updating contact-amounts setting: " + errorStringForLog(err);
      this.$logAndConsole(fullError, true);
      // Use notification helper and constant
      this.notify.error(
        NOTIFY_CONTACT_SETTING_SAVE_ERROR.message,
        TIMEOUTS.LONG,
      );
    }
    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,
    };
  }

  /**
   * Copy selected contacts as a shareable JWT URL
   * Creates a JWT containing selected contact data and copies to clipboard
   */
  private async copySelectedContacts() {
    if (this.contactsSelected.length === 0) {
      // Use notification helper and constant
      this.notify.error(NOTIFY_CONTACTS_SELECT_TO_COPY.message);
      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,
    });
    // Use production URL for sharing to avoid localhost issues in development
    const contactsJwtUrl = `${APP_SERVER}/deep-link/contact-import/${contactsJwt}`;
    try {
      await copyToClipboard(contactsJwtUrl);
      // Use notification helper
      this.notify.copied(NOTIFY_CONTACT_LINK_COPIED.message);
    } catch (error) {
      this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
      this.notify.error("Failed to copy to clipboard. Please try again.");
    }
  }

  private showCopySelectionsInfo() {
    // Use notification helper and constant
    this.notify.info(NOTIFY_CONTACT_INFO_COPY.message, TIMEOUTS.LONG);
  }

  /**
   * Show onboarding meeting dialog based on user's meeting status
   * Checks if user is in a meeting and whether they are the host
   */
  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) {
      this.$logAndConsole(
        "Error checking meeting status:" + errorStringForLog(error),
      );
      // Use notification helper
      this.notify.error(NOTIFY_MEETING_STATUS_ERROR.message);
    }
  }

  /**
   * Handle QR code button click - route to appropriate scanner
   * Uses QRNavigationService to determine scanner type and route
   */
  public handleQRCodeClick() {
    this.$logAndConsole(
      "[ContactsView] handleQRCodeClick method called",
      false,
    );

    const qrNavigationService = QRNavigationService.getInstance();
    const route = qrNavigationService.getQRScannerRoute();

    this.$router.push(route);
  }

  /**
   * Show export data prompt after adding a contact
   * Prompts user to export their contact data as a backup
   */
  private async showExportDataPrompt(): Promise<void> {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: NOTIFY_EXPORT_DATA_PROMPT.title,
        text: NOTIFY_EXPORT_DATA_PROMPT.message,
        onYes: async () => {
          await this.exportContactData();
        },
        yesText: "Export Data",
        onNo: async () => {
          // User chose not to export - no action needed
        },
        noText: "Not Yet",
      },
      -1,
    );
  }

  /**
   * Export contact data to JSON file
   * Exports contacts and contact labels tables
   * Uses platform service to handle platform-specific export logic
   */
  private async exportContactData(): Promise<void> {
    try {
      // Fetch all contacts from database
      // Prepare export data using shared utility function
      await this.$saveContactExport();

      this.notify.success(
        "Contact export completed successfully. Check downloads or the share dialog.",
      );
    } catch (error) {
      logger.error("Export Error:", error);
      this.notify.error(
        `There was an error exporting the data: ${error instanceof Error ? error.message : "Unknown error"}`,
      );
    }
  }
}
</script>
