<template>
  <!-- CONTENT -->
  <section id="Content" class="relative w-[100vw] h-[100vh]">
    <div :class="mainContentClasses">
      <div class="mb-4">
        <h1 class="text-xl text-center font-semibold relative">
          <!-- Back -->
          <a
            class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
            @click="handleBack"
          >
            <font-awesome icon="chevron-left" class="fa-fw" />
          </a>

          <!-- Quick Help -->
          <a
            class="text-xl text-center text-blue-500 px-2 py-1 absolute -right-2 -top-1"
            @click="toastQRCodeHelp()"
          >
            <font-awesome icon="circle-question" class="fa-fw" />
          </a>

          Share Contact Info
        </h1>
      </div>

      <div
        v-if="shouldShowNameWarning"
        class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
      >
        <p class="mb-2">
          <b>Note:</b> your identity currently does <b>not</b> include a name.
        </p>
        <button
          class="inline-block text-md 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-4 py-2 rounded-md"
          @click="openUserNameDialog"
        >
          Set Your Name
        </button>
      </div>

      <UserNameDialog ref="userNameDialog" />

      <div
        v-if="hasEthrDid"
        :class="qrContainerClasses"
        @click="onCopyUrlToClipboard()"
      >
        <!--
          Play with display options: https://qr-code-styling.com/
          See docs: https://www.npmjs.com/package/qr-code-generator-vue3
        -->
        <QRCodeVue3
          :value="qrValue"
          :width="606"
          :height="606"
          :corners-square-options="{ type: 'square' }"
          :dots-options="{ type: 'square', color: '#000' }"
        />
      </div>

      <div v-else-if="hasAnyDid" class="text-center mt-4">
        <!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) -->
        <span class="text-blue-500" @click="onCopyDidToClipboard()">
          Click here to copy your DID to your clipboard.
        </span>
        <span>
          Then give it to them so they can paste it in their list of People.
        </span>
      </div>

      <div v-else class="text-center mt-4">
        You have no identitifiers yet, so
        <router-link
          :to="{ name: 'start' }"
          class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
        >
          create your identifier.
        </router-link>
        <br />
        If you don't do that first, these contacts won't see your activity.
      </div>
    </div>

    <div :class="cameraFrameClasses">
      <p
        class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10"
      >
        Position QR code in the frame
      </p>

      <p
        v-if="error"
        class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-sm text-center py-2 z-20 text-rose-400"
      >
        {{ error }}
      </p>
    </div>
  </section>
</template>

<script lang="ts">
import { Buffer } from "buffer/";
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";

import { logger } from "../utils/logger";
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
import QuickNav from "../components/QuickNav.vue";
import { Contact } from "../db/tables/contacts";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import * as libsUtil from "../libs/util";
import {
  CONTACT_CSV_HEADER,
  CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
  generateEndorserJwtUrlForAccount,
  setVisibilityUtil,
} from "../libs/endorserServer";
import UserNameDialog from "../components/UserNameDialog.vue";
import { retrieveAccountMetadata } from "../libs/util";
import { parseJsonField } from "../db/databaseUtil";
import { Account } from "@/db/tables/accounts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import {
  NOTIFY_QR_INITIALIZATION_ERROR,
  NOTIFY_QR_HTTPS_REQUIRED,
  NOTIFY_QR_CAMERA_ACCESS_REQUIRED,
  NOTIFY_QR_CONTACT_EXISTS,
  NOTIFY_QR_CONTACT_ERROR,
  NOTIFY_QR_INVALID_QR_CODE,
  NOTIFY_QR_INVALID_CONTACT_INFO,
  NOTIFY_QR_MISSING_DID,
  NOTIFY_QR_UNKNOWN_CONTACT_TYPE,
  NOTIFY_QR_PROCESSING_ERROR,
  NOTIFY_QR_URL_COPIED,
  NOTIFY_QR_CODE_HELP,
  NOTIFY_QR_DID_COPIED,
  createQRContactAddedMessage,
  QR_TIMEOUT_MEDIUM,
  QR_TIMEOUT_STANDARD,
  QR_TIMEOUT_LONG,
} from "@/constants/notifications";
import { createNotifyHelpers } from "../utils/notify";

interface QRScanResult {
  rawValue?: string;
  barcode?: string;
}

interface IUserNameDialog {
  open: (callback: (name: string) => void) => void;
}

@Component({
  components: {
    QuickNav,
    QRCodeVue3,
    UserNameDialog,
  },
  mixins: [PlatformServiceMixin],
})
/**
 * ContactQRScanFullView.vue
 *
 * Enhanced QR code scanner component for exchanging contact information in TimeSafari.
 * Supports both sharing user's contact info via QR code and scanning other users' QR codes.
 *
 * Key Features:
 * - Generates QR codes for user's contact information
 * - Scans QR codes from other TimeSafari users
 * - Handles both JWT-based and CSV-based contact formats
 * - Debounces duplicate scans to prevent processing same code multiple times
 * - Manages camera permissions and lifecycle
 * - Provides real-time feedback during scanning process
 *
 * Database Operations:
 * - Retrieves user settings and profile information
 * - Stores new contacts with proper validation
 * - Manages contact visibility settings
 *
 * Security Features:
 * - Validates contact information before storage
 * - Encrypts sensitive data using platform services
 * - Handles camera permissions securely
 * - Prevents duplicate contact entries
 *
 * @author Matthew Raymer
 * @since 2024
 */
export default class ContactQRScanFull extends Vue {
  $notify!: (notification: any, timeout?: number) => void;
  $router!: Router;

  // Notification helper system
  private notify = createNotifyHelpers(this.$notify);

  isScanning = false;
  error: string | null = null;
  activeDid = "";
  apiServer = "";
  givenName = "";
  isRegistered = false;
  profileImageUrl = "";
  qrValue = "";
  ETHR_DID_PREFIX = ETHR_DID_PREFIX;

  // Add new properties to track scanning state
  private lastScannedValue: string = "";
  private lastScanTime: number = 0;
  private readonly SCAN_DEBOUNCE_MS = 5000; // Increased from 2000 to 5000ms to better handle mobile scanning

  // Add cleanup tracking
  private isCleaningUp = false;
  private isMounted = false;

  /**
   * Computed property for QR code container CSS classes
   */
  get qrContainerClasses(): string {
    return "block w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto mt-4";
  }

  /**
   * Computed property for camera frame CSS classes
   */
  get cameraFrameClasses(): string {
    return "relative w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto border border-dashed border-white mt-8 aspect-square";
  }

  /**
   * Computed property for main content container CSS classes
   */
  get mainContentClasses(): string {
    return "p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto";
  }

  /**
   * Computed property to determine if user has an ETHR DID
   */
  get hasEthrDid(): boolean {
    return !!(this.activeDid && this.activeDid.startsWith(ETHR_DID_PREFIX));
  }

  /**
   * Computed property to determine if user has any DID
   */
  get hasAnyDid(): boolean {
    return !!this.activeDid;
  }

  /**
   * Computed property to determine if user should be shown the name setup warning
   */
  get shouldShowNameWarning(): boolean {
    return !this.givenName;
  }

  /**
   * Vue lifecycle hook - component initialization
   * Loads user settings and generates QR code for contact sharing
   */
  async created() {
    try {
      const settings = await this.$accountSettings();
      this.activeDid = settings.activeDid || "";
      this.apiServer = settings.apiServer || "";
      this.givenName = settings.firstName || "";
      this.isRegistered = !!settings.isRegistered;
      this.profileImageUrl = settings.profileImageUrl || "";

      const account = await retrieveAccountMetadata(this.activeDid);
      if (account) {
        const name =
          (settings.firstName || "") +
          (settings.lastName ? ` ${settings.lastName}` : "");
        const publicKeyBase64 = Buffer.from(
          account.publicKeyHex,
          "hex",
        ).toString("base64");
        this.qrValue =
          CONTACT_CSV_HEADER +
          "\n" +
          `"${name}",${account.did},${publicKeyBase64},false,${this.isRegistered}`;
      }
    } catch (error) {
      logger.error("Error initializing component:", {
        error: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined,
      });
      this.notify.error(
        NOTIFY_QR_INITIALIZATION_ERROR.message,
        QR_TIMEOUT_LONG,
      );
    }
  }

  /**
   * Starts the QR code scanning process
   * Handles permission requests and initializes camera access
   * @throws Will log error and show notification if scanning fails to start
   */
  async startScanning() {
    if (this.isCleaningUp) {
      logger.debug("Cannot start scanning during cleanup");
      return;
    }

    try {
      this.error = null;
      this.isScanning = true;
      this.lastScannedValue = "";
      this.lastScanTime = 0;

      const scanner = QRScannerFactory.getInstance();

      // Check if scanning is supported first
      if (!(await scanner.isSupported())) {
        this.error =
          "Camera access requires HTTPS. Please use a secure connection.";
        this.isScanning = false;
        this.notify.warning(NOTIFY_QR_HTTPS_REQUIRED.message, QR_TIMEOUT_LONG);
        return;
      }

      // Check permissions first
      if (!(await scanner.checkPermissions())) {
        const granted = await scanner.requestPermissions();
        if (!granted) {
          this.error = "Camera permission denied";
          this.isScanning = false;
          // Show notification for better visibility
          this.notify.warning(
            NOTIFY_QR_CAMERA_ACCESS_REQUIRED.message,
            QR_TIMEOUT_LONG,
          );
          return;
        }
      }

      // Add scan listener
      scanner.addListener({
        onScan: this.onScanDetect,
        onError: this.onScanError,
      });

      // Start scanning
      await scanner.startScan();
    } catch (error) {
      this.error = error instanceof Error ? error.message : String(error);
      this.isScanning = false;
      logger.error("Error starting scan:", {
        error: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined,
      });
    }
  }

  /**
   * Stops the QR code scanning process and cleans up scan state
   * @throws Will log error if stopping scan fails
   */
  async stopScanning() {
    try {
      const scanner = QRScannerFactory.getInstance();
      await scanner.stopScan();
    } catch (error) {
      logger.error("Error stopping scan:", {
        error: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined,
      });
    } finally {
      this.isScanning = false;
      this.lastScannedValue = "";
      this.lastScanTime = 0;
    }
  }

  /**
   * Cleans up QR scanner resources and prevents memory leaks
   * @throws Will log error if cleanup fails
   */
  async cleanupScanner() {
    if (this.isCleaningUp) {
      return;
    }

    this.isCleaningUp = true;
    try {
      logger.info("Cleaning up QR scanner resources");
      await this.stopScanning();
      await QRScannerFactory.cleanup();
    } catch (error) {
      logger.error("Error during scanner cleanup:", {
        error: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined,
      });
    } finally {
      this.isCleaningUp = false;
    }
  }

  /**
   * Handle QR code scan result with debouncing to prevent duplicate scans
   */
  async onScanDetect(result: string | QRScanResult) {
    try {
      // Extract raw value from different possible formats
      const rawValue =
        typeof result === "string"
          ? result
          : result?.rawValue || result?.barcode;
      if (!rawValue) {
        logger.warn("Invalid scan result - no value found:", result);
        return;
      }

      // Debounce duplicate scans
      const now = Date.now();
      if (
        rawValue === this.lastScannedValue &&
        now - this.lastScanTime < this.SCAN_DEBOUNCE_MS
      ) {
        logger.info("Ignoring duplicate scan:", rawValue);
        return;
      }

      // Update scan tracking
      this.lastScannedValue = rawValue;
      this.lastScanTime = now;

      logger.info("Processing QR code scan result:", rawValue);

      let contact: Contact;
      if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
        // Extract JWT
        const jwt = getContactJwtFromJwtUrl(rawValue);
        if (!jwt) {
          logger.warn("Invalid QR code format - no JWT found in URL");
          this.notify.error(NOTIFY_QR_INVALID_QR_CODE.message, QR_TIMEOUT_LONG);
          return;
        }

        // Process JWT and contact info
        logger.info("Decoding JWT payload from QR code");
        const decodedJwt = await decodeEndorserJwt(jwt);
        if (!decodedJwt?.payload?.own) {
          logger.warn("Invalid JWT payload - missing 'own' field");
          this.notify.error(
            NOTIFY_QR_INVALID_CONTACT_INFO.message,
            QR_TIMEOUT_LONG,
          );
          return;
        }

        const contactInfo = decodedJwt.payload.own;
        const did = contactInfo.did || decodedJwt.payload.iss;
        if (!did) {
          logger.warn("Invalid contact info - missing DID");
          this.notify.error(NOTIFY_QR_MISSING_DID.message, QR_TIMEOUT_LONG);
          return;
        }

        // Create contact object
        contact = {
          did: did,
          name: contactInfo.name || "",
          publicKeyBase64: contactInfo.publicKeyBase64 || "",
          seesMe: contactInfo.seesMe || false,
          registered: contactInfo.registered || false,
        };
      } else if (rawValue.startsWith(CONTACT_CSV_HEADER)) {
        const lines = rawValue.split(/\n/);
        contact = libsUtil.csvLineToContact(lines[1]);
      } else {
        this.notify.error(
          NOTIFY_QR_UNKNOWN_CONTACT_TYPE.message,
          QR_TIMEOUT_LONG,
        );
        return;
      }

      // Add contact but keep scanning
      logger.info("Adding new contact to database:", {
        did: contact.did,
        name: contact.name,
      });
      await this.addNewContact(contact);
    } catch (error) {
      logger.error("Error processing contact QR code:", {
        error: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined,
      });
      this.notify.error(
        error instanceof Error
          ? error.message
          : NOTIFY_QR_PROCESSING_ERROR.message,
        QR_TIMEOUT_LONG,
      );
    }
  }

  /**
   * Handles QR code scan errors
   * @param error - Error object from scanner
   */
  onScanError(error: Error) {
    this.error = error.message;
    logger.error("QR code scan error:", {
      error: error.message,
      stack: error.stack,
    });
  }

  /**
   * Sets the visibility of a contact (whether they can see user's activity)
   * @param contact - Contact object to set visibility for
   * @param visibility - Whether contact should be able to see user's activity
   * @throws Will log error and show notification if visibility setting fails
   */
  async setVisibility(contact: Contact, visibility: boolean) {
    const result = await setVisibilityUtil(
      this.activeDid,
      this.apiServer,
      this.axios,
      contact,
      visibility,
    );
    if (result.error) {
      this.notify.error(result.error as string, QR_TIMEOUT_LONG);
    } else if (!result.success) {
      logger.warn("Unexpected result from setting visibility:", result);
    }
  }

  /**
   * Adds a new contact to the database after validation
   * @param contact - Contact object to add
   * @throws Will log error and show notification if contact addition fails
   */
  async addNewContact(contact: Contact) {
    try {
      logger.info("Opening database connection for new contact");

      // Check if contact already exists
      const existingContact = await this.$getContact(contact.did);

      if (existingContact) {
        logger.info("Contact already exists", { did: contact.did });
        this.notify.warning(NOTIFY_QR_CONTACT_EXISTS.message, QR_TIMEOUT_LONG);
        return;
      }

      // Add new contact
      // @ts-expect-error because we're just using the value to store to the DB
      contact.contactMethods = JSON.stringify(
        parseJsonField(contact.contactMethods, []),
      );
      await this.$insertContact(contact);

      if (this.activeDid) {
        logger.info("Setting contact visibility", { did: contact.did });
        await this.setVisibility(contact, true);
        contact.seesMe = true;
      }

      this.notify.success(
        createQRContactAddedMessage(!!this.activeDid),
        QR_TIMEOUT_STANDARD,
      );
    } catch (error) {
      logger.error("Error saving contact to database:", {
        did: contact.did,
        error: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined,
      });
      this.notify.error(NOTIFY_QR_CONTACT_ERROR.message, QR_TIMEOUT_LONG);
    }
  }

  /**
   * Vue lifecycle hook - component mounted
   * Sets up event listeners and starts scanning automatically
   */
  mounted() {
    this.isMounted = true;
    document.addEventListener("pause", this.handleAppPause);
    document.addEventListener("resume", this.handleAppResume);
    this.startScanning(); // Automatically start scanning when view is mounted
  }

  /**
   * Vue lifecycle hook - component before destruction
   * Cleans up event listeners and scanner resources
   */
  beforeDestroy() {
    this.isMounted = false;
    document.removeEventListener("pause", this.handleAppPause);
    document.removeEventListener("resume", this.handleAppResume);
    this.cleanupScanner();
  }

  /**
   * Handles app pause event by stopping scanner
   */
  async handleAppPause() {
    if (!this.isMounted) return;

    logger.info("App paused, stopping scanner");
    await this.stopScanning();
  }

  /**
   * Handles app resume event by resetting scanner state
   */
  handleAppResume() {
    if (!this.isMounted) return;

    logger.info("App resumed, scanner can be restarted by user");
    this.isScanning = false;
  }

  /**
   * Handles back navigation with proper cleanup
   */
  async handleBack() {
    await this.cleanupScanner();
    this.$router.back();
  }

  /**
   * Shows help notification about QR code functionality
   */
  toastQRCodeHelp() {
    this.notify.info(NOTIFY_QR_CODE_HELP.message, QR_TIMEOUT_LONG);
  }

  /**
   * Copies contact URL to clipboard for sharing
   */
  async onCopyUrlToClipboard() {
    const account = (await libsUtil.retrieveFullyDecryptedAccount(
      this.activeDid,
    )) as Account;
    const jwtUrl = await generateEndorserJwtUrlForAccount(
      account,
      this.isRegistered,
      this.givenName,
      this.profileImageUrl,
      true,
    );
    useClipboard()
      .copy(jwtUrl)
      .then(() => {
        this.notify.toast(
          NOTIFY_QR_URL_COPIED.title,
          NOTIFY_QR_URL_COPIED.message,
          QR_TIMEOUT_MEDIUM,
        );
      });
  }

  /**
   * Copies DID to clipboard for manual sharing
   */
  onCopyDidToClipboard() {
    useClipboard()
      .copy(this.activeDid)
      .then(() => {
        this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
      });
  }

  /**
   * Opens the user name dialog for setting user's display name
   */
  openUserNameDialog() {
    (this.$refs.userNameDialog as IUserNameDialog).open((name: string) => {
      this.givenName = name;
    });
  }
}
</script>

<style scoped>
.aspect-square {
  aspect-ratio: 1 / 1;
}
</style>
