<template>
  <QuickNav selected="Invite" />
  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
    <div
      v-if="checkingInvite"
      class="text-lg text-center font-light relative px-7"
    >
      <font-awesome icon="spinner" class="fa-spin-pulse" />
    </div>
    <div v-else class="text-center mt-4">
      <p>That invitation did not work.</p>
      <p class="mt-2">
        Go back to your invite message and copy the entire text, then paste it
        here.
      </p>
      <p class="mt-2">
        If the data looks correct, try Chrome. (For example, iOS may have cut
        off the invite data, or it may have shown a preview that stole your
        invite.) If it still complains, you may need the person who invited you
        to send a new one.
      </p>
      <textarea
        v-model="inputJwt"
        placeholder="Paste invitation..."
        class="mt-4 border-2 border-gray-300 p-2 rounded"
        cols="30"
        @input="() => checkInvite(inputJwt)"
      />
      <br />
      <button
        class="ml-2 p-2 bg-blue-500 text-white rounded"
        @click="() => processInvite(inputJwt, true)"
      >
        Accept
      </button>
    </div>
  </section>
</template>

<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router, RouteLocationNormalized } from "vue-router";

import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import {
  db,
  logConsoleAndDb,
  retrieveSettingsForActiveAccount,
} from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { decodeEndorserJwt } from "../libs/crypto/vc";
import { errorStringForLog } from "../libs/endorserServer";
import { generateSaveAndActivateIdentity } from "../libs/util";

/**
 * Invite One Accept View Component
 * @author Matthew Raymer
 *
 * This component handles accepting single-use invitations to join the platform.
 * It supports multiple invitation formats and provides user feedback during the process.
 *
 * Workflow:
 * 1. Component loads with JWT from route or user input
 * 2. Validates JWT format and signature
 * 3. Processes invite data and redirects to contacts page
 * 4. Handles errors with user feedback
 *
 * Supported Invite Formats:
 * 1. Direct JWT in URL path: /invite-one-accept/{jwt}
 * 2. JWT in text message URL: https://app.example.com/invite-one-accept/{jwt}
 * 3. JWT surrounded by other text: "Your invite code is {jwt}"
 *
 * Security Features:
 * - JWT validation
 * - Identity generation if needed
 * - Error handling for invalid/expired invites
 *
 * @see ContactsView for completion of invite process
 */
@Component({
  components: { QuickNav },
})
export default class InviteOneAcceptView extends Vue {
  /** Notification function injected by Vue */
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  /** Router instance for navigation */
  $router!: Router;
  /** Route instance for current route */
  $route!: RouteLocationNormalized;

  /** Active user's DID */
  activeDid = "";
  /** API server endpoint */
  apiServer = "";
  /** Loading state for invite processing */
  checkingInvite = true;
  /** User input for manual JWT entry */
  inputJwt = "";

  /**
   * Component lifecycle hook that initializes invite processing
   *
   * Workflow:
   * 1. Opens database connection
   * 2. Retrieves account settings
   * 3. Ensures active DID exists or generates one
   * 4. Extracts JWT from URL path
   * 5. Processes invite automatically
   *
   * @throws Will not throw but logs errors
   * @emits Notifications on errors
   */
  async mounted() {
    this.checkingInvite = true;

    // Load or generate identity
    let settings = await databaseUtil.retrieveSettingsForActiveAccount();
    if (USE_DEXIE_DB) {
      await db.open();
      settings = await retrieveSettingsForActiveAccount();
    }
    this.activeDid = settings.activeDid || "";
    this.apiServer = settings.apiServer || "";

    if (!this.activeDid) {
      this.activeDid = await generateSaveAndActivateIdentity();
    }

    // Extract JWT from route path
    const jwt = (this.$route.params.jwt as string) || "";
    await this.processInvite(jwt, false);

    this.checkingInvite = false;
  }

  /**
   * Processes an invite JWT and/or text containing the invite
   *
   * Handles multiple input formats:
   * 1. Direct JWT:
   *    - Raw JWT string starting with "ey"
   *    - Example: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ...
   *
   * 2. URL containing JWT:
   *    - Full URL with JWT in path
   *    - Pattern: /invite-one-accept/{jwt}
   *    - Example: https://app.example.com/invite-one-accept/eyJ0eXAiOiJKV1Q...
   *
   * 3. Text with embedded JWT:
   *    - JWT surrounded by other text
   *    - Uses regex to extract JWT pattern
   *    - Example: "Your invite code is eyJ0eXAiOiJKV1Q... Click to accept"
   *
   * Extraction Process:
   * 1. First attempts URL pattern match
   * 2. If no URL found, looks for JWT pattern (ey...)
   * 3. Validates extracted JWT format
   * 4. Redirects to contacts page on success
   *
   * Error Handling:
   * - Missing JWT: Shows "Missing Invite" notification
   * - Invalid JWT: Logs error and shows generic error message
   * - Network Issues: Captured in try/catch block
   *
   * @param jwtInput Raw input that may contain a JWT
   * @param notifyOnFailure Whether to show error notifications
   *        - true: Shows UI notifications for errors
   *        - false: Silently logs errors (used for auto-processing)
   * @throws Will not throw but logs errors
   * @emits Notifications on errors if notifyOnFailure is true
   * @emits Router navigation on success to /contacts?inviteJwt={jwt}
   */
  async processInvite(jwtInput: string, notifyOnFailure: boolean) {
    this.checkingInvite = true;

    try {
      const jwt = this.extractJwtFromInput(jwtInput);

      if (!jwt) {
        this.handleMissingJwt(notifyOnFailure);
        return;
      }

      await this.validateAndRedirect(jwt);
    } catch (error) {
      this.handleError(error, notifyOnFailure);
    } finally {
      this.checkingInvite = false;
    }
  }

  /**
   * Extracts JWT from various input formats
   * @param input Raw input text
   * @returns Extracted JWT or empty string
   */
  private extractJwtFromInput(input: string): string {
    const jwtInput = input ?? "";

    // Try URL format first
    const urlMatch = jwtInput.match(/(https?:\/\/[^\s]+)/);
    if (urlMatch?.[1]) {
      const internalMatch = urlMatch[1].match(/\/invite-one-accept\/([^?]+)/);
      if (internalMatch?.[1]) return internalMatch[1];
    }

    // Try direct JWT format
    const spaceMatch = jwtInput.match(/(ey[\w.-]+)/);
    if (spaceMatch?.[1]) return spaceMatch[1];

    return "";
  }

  /**
   * Validates JWT and redirects to contacts page
   * @param jwt JWT to validate
   */
  private async validateAndRedirect(jwt: string) {
    decodeEndorserJwt(jwt);
    this.$router.push({
      name: "contacts",
      query: { inviteJwt: jwt },
    });
  }

  /**
   * Handles missing JWT error
   * @param notify Whether to show notification
   */
  private handleMissingJwt(notify: boolean) {
    if (notify) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Missing Invite",
          text: "There was no invite. Paste the entire text that has the data.",
        },
        5000,
      );
    }
  }

  /**
   * Handles processing errors
   * @param error Error that occurred
   * @param notify Whether to show notification
   */
  private handleError(error: unknown, notify: boolean) {
    const fullError = "Error accepting invite: " + errorStringForLog(error);
    logConsoleAndDb(fullError, true);

    if (notify) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "There was an error processing that invite.",
        },
        3000,
      );
    }
  }

  /**
   * Validates invite data format
   *
   * Checks for common error cases:
   * - Truncated URLs
   * - Missing JWT data
   * - Invalid URL formats
   *
   * @param jwtInput Raw input to validate
   * @throws Will not throw but shows notifications
   * @emits Notifications on validation errors
   */
  async checkInvite(jwtInput: string) {
    if (
      jwtInput.endsWith(APP_SERVER) ||
      jwtInput.endsWith(APP_SERVER + "/") ||
      jwtInput.endsWith("invite-one-accept") ||
      jwtInput.endsWith("invite-one-accept/")
    ) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: "That is only part of the invite data; it's missing some at the end. Try another way to get the full data.",
        },
        5000,
      );
    }
  }
}
</script>
