<template>
  <QuickNav selected="Invite" />
  <TopMessage />

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

    <!-- Heading -->
    <h1 class="text-4xl text-center font-light">Invitations</h1>

    <ul class="ml-8 mt-4 list-outside list-disc w-5/6">
      <li>
        Note when sending
        <span
          v-if="!showAppleWarning"
          class="text-blue-500 cursor-pointer"
          @click="showAppleWarning = !showAppleWarning"
        >
          to Apple users...
        </span>
        <span v-else>
          to Apple users: their links often fail because their device cuts off
          part of the link. You might need to send it to them some other way,
          like in an email.
        </span>
      </li>
    </ul>

    <!-- New Project -->
    <button
      v-if="isRegistered"
      class="fixed right-6 top-12 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
      @click="createInvite()"
    >
      <font-awesome icon="plus" class="fa-fw"></font-awesome>
    </button>

    <InviteDialog ref="inviteDialog" />

    <!-- Invites Table -->
    <div v-if="invites.length" class="mt-6">
      <table class="min-w-full bg-white">
        <thead>
          <tr>
            <th class="py-2">
              ID
              <br />
              (click for link)
            </th>
            <th class="py-2">Notes</th>
            <th class="py-2">Expires At</th>
            <th class="py-2">Redeemed</th>
          </tr>
        </thead>
        <tbody>
          <tr
            v-for="invite in invites"
            :key="invite.inviteIdentifier"
            class="border-t py-2"
          >
            <td>
              <span
                v-if="isInviteActive(invite)"
                :class="activeInviteClass"
                :title="inviteLink(invite.jwt)"
                @click="
                  copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)
                "
              >
                {{ getTruncatedInviteId(invite.inviteIdentifier) }}
              </span>
              <span
                v-else
                :class="inactiveInviteClass"
                :title="invite.inviteIdentifier"
                @click="
                  showInvite(
                    invite.inviteIdentifier,
                    !!invite.redeemedAt,
                    isInviteExpired(invite),
                  )
                "
              >
                {{ getTruncatedInviteId(invite.inviteIdentifier) }}
              </span>
            </td>
            <td class="text-left" :data-testId="inviteLink(invite.jwt)">
              {{ invite.notes }}
            </td>
            <td class="text-center">
              {{ formatExpirationDate(invite) }}
            </td>
            <td class="text-center">
              {{ formatRedemptionDate(invite) }}
              <br />
              {{ getTruncatedRedeemedBy(invite.redeemedBy) }}
              <br />
              <font-awesome
                v-if="invite.redeemedBy && !contactsRedeemed[invite.redeemedBy]"
                icon="plus"
                class="bg-green-600 text-white px-1 py-1 rounded-full cursor-pointer"
                @click="addNewContact(invite.redeemedBy, invite.notes)"
              />
            </td>
            <td>
              <font-awesome
                icon="trash-can"
                class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
                @click="deleteInvite(invite.inviteIdentifier, invite.notes)"
              />
            </td>
          </tr>
        </tbody>
      </table>
      <ContactNameDialog ref="contactNameDialog" />
    </div>
    <p v-else class="mt-6 text-center">No invites found.</p>
  </section>
</template>
<script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import { copyToClipboard } from "../services/ClipboardService";
import { Router } from "vue-router";

import ContactNameDialog from "../components/ContactNameDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import InviteDialog from "../components/InviteDialog.vue";
import { AppString, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { createInviteJwt, getHeaders } from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { APP_SERVER } from "@/constants/app";
import {
  NOTIFY_INVITE_LOAD_ERROR,
  NOTIFY_INVITE_DELETED,
  createInviteLinkCopyMessage,
  createInviteIdCopyMessage,
  createContactAddedMessage,
  createInviteDeleteConfirmMessage,
} from "@/constants/notifications";

/**
 * InviteOneView Component
 *
 * Manages user invitations with comprehensive invite lifecycle management.
 * Handles creation, tracking, deletion, and redemption of invitations.
 * Integrates with contact management for redeemed invitations.
 *
 * Key features:
 * - Invitation lifecycle management (create, track, delete)
 * - Contact integration for redeemed invitations
 * - Link generation and clipboard functionality
 * - Comprehensive error handling and user feedback
 * - Expiration and status tracking
 *
 * Database Operations:
 * - Account settings retrieval via PlatformServiceMixin
 * - Contact queries and management via mixin methods
 * - Contact insertion for redeemed invitations
 *
 * Migration Status: Phase 1 Complete - Database patterns modernized
 */

interface Invite {
  inviteIdentifier: string;
  expiresAt: string;
  jwt: string;
  notes: string;
  redeemedAt: string | null;
  redeemedBy: string | null;
}

@Component({
  components: { ContactNameDialog, QuickNav, TopMessage, InviteDialog },
  mixins: [PlatformServiceMixin],
})
export default class InviteOneView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  $router!: Router;

  invites: Invite[] = [];
  activeDid: string = "";
  apiServer: string = "";
  contactsRedeemed: { [key: string]: Contact } = {};
  isRegistered: boolean = false;
  showAppleWarning = false;

  notify!: ReturnType<typeof createNotifyHelpers>;
  /** Production share domain for deep links */
  APP_SERVER = APP_SERVER;

  /**
   * Initializes notification helpers
   */
  created() {
    this.notify = createNotifyHelpers(this.$notify);
  }

  // =================================================
  // COMPUTED PROPERTIES
  // =================================================

  /**
   * CSS classes for active invite links
   */
  get activeInviteClass(): string {
    return "text-center text-blue-500 cursor-pointer";
  }

  /**
   * CSS classes for inactive invite links
   */
  get inactiveInviteClass(): string {
    return "text-center text-slate-500 cursor-pointer";
  }

  // =================================================
  // HELPER METHODS
  // =================================================

  /**
   * Checks if an invite is currently active (not redeemed and not expired)
   * @param invite - The invite to check
   * @returns True if invite is active
   */
  isInviteActive(invite: Invite): boolean {
    return !invite.redeemedAt && invite.expiresAt > new Date().toISOString();
  }

  /**
   * Checks if an invite has expired
   * @param invite - The invite to check
   * @returns True if invite is expired
   */
  isInviteExpired(invite: Invite): boolean {
    return invite.expiresAt < new Date().toISOString();
  }

  /**
   * Formats expiration date for display
   * @param invite - The invite to format
   * @returns Formatted date string or empty if redeemed
   */
  formatExpirationDate(invite: Invite): string {
    return invite.redeemedAt ? "" : invite.expiresAt.substring(0, 10);
  }

  /**
   * Formats redemption date for display
   * @param invite - The invite to format
   * @returns Formatted date string or empty if not redeemed
   */
  formatRedemptionDate(invite: Invite): string {
    return invite.redeemedAt?.substring(0, 10) || "";
  }

  // =================================================
  // LIFECYCLE METHODS
  // =================================================

  /**
   * Initializes component with user invitations and contact data
   *
   * Workflow:
   * 1. Retrieves account settings via PlatformServiceMixin
   * 2. Loads user invitations from server API
   * 3. Loads contact data for redeemed invitations
   * 4. Maps redeemed contacts for display
   */
  async mounted() {
    try {
      // Use PlatformServiceMixin for account settings
      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 || "";
      this.isRegistered = !!settings.isRegistered;

      const headers = await getHeaders(this.activeDid);
      const response = await axios.get(
        this.apiServer + "/api/userUtil/invite",
        { headers },
      );
      this.invites = response.data.data;

      // Use PlatformServiceMixin for contact retrieval
      const allContacts = await this.$getAllContacts();

      // Map redeemed contacts for display
      for (const invite of this.invites) {
        const contact = allContacts.find(
          (contact) => contact.did === invite.redeemedBy,
        );
        if (contact && invite.redeemedBy) {
          this.contactsRedeemed[invite.redeemedBy] = contact;
        }
      }
    } catch (error) {
      logger.error("Error fetching invites:", error);
      this.notify.error(NOTIFY_INVITE_LOAD_ERROR.message, TIMEOUTS.LONG);
    }
  }

  getTruncatedInviteId(inviteId: string): string {
    if (inviteId.length <= 9) return inviteId;
    return `${inviteId.slice(0, 6)}...`;
  }

  getTruncatedRedeemedBy(redeemedBy: string | null): string {
    if (!redeemedBy) return "";
    if (this.contactsRedeemed[redeemedBy]) {
      return (
        this.contactsRedeemed[redeemedBy].name || AppString.NO_CONTACT_NAME
      );
    }
    if (redeemedBy.length <= 19) return redeemedBy;
    return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
  }

  inviteLink(jwt: string): string {
    // Use production URL for sharing to avoid localhost issues in development
    return `${APP_SERVER}/deep-link/invite-one-accept/${jwt}`;
  }

  async copyInviteAndNotify(inviteId: string, jwt: string) {
    try {
      await copyToClipboard(this.inviteLink(jwt));
      this.notify.success(createInviteLinkCopyMessage(inviteId), TIMEOUTS.LONG);
    } catch (error) {
      this.$logAndConsole(`Error copying invite link: ${error}`, true);
      this.notify.error("Failed to copy invite link.");
    }
  }

  async showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
    try {
      await copyToClipboard(inviteId);
      this.notify.success(
        createInviteIdCopyMessage(inviteId, redeemed, expired),
        TIMEOUTS.LONG,
      );
    } catch (error) {
      this.$logAndConsole(`Error copying invite ID: ${error}`, true);
      this.notify.error("Failed to copy invite ID.");
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  lookForErrorAndNotify(error: any, title: string, defaultMessage: string) {
    logger.error(title, "-", error);
    let message = defaultMessage;
    if (error.response && error.response.data && error.response.data.error) {
      if (error.response.data.error.message) {
        message = error.response.data.error.message;
      } else {
        message = error.response.data.error;
      }
    }
    this.notify.error(message, TIMEOUTS.LONG);
  }

  async createInvite() {
    const inviteIdentifier =
      Math.random().toString(36).substring(2) +
      Math.random().toString(36).substring(2) +
      Math.random().toString(36).substring(2);
    (this.$refs.inviteDialog as InviteDialog).open(
      inviteIdentifier,
      async (notes, expiresAt) => {
        try {
          const headers = await getHeaders(this.activeDid);
          if (!expiresAt) {
            throw {
              response: {
                data: { error: "You must select an expiration date." },
              },
            };
          }
          const expiresIn = (new Date(expiresAt).getTime() - Date.now()) / 1000;
          const inviteJwt = await createInviteJwt(
            this.activeDid,
            undefined,
            inviteIdentifier,
            expiresIn,
          );
          await axios.post(
            this.apiServer + "/api/userUtil/invite",
            { inviteJwt, notes, expiresAt },
            { headers },
          );
          const newInvite = {
            inviteIdentifier: inviteIdentifier,
            expiresAt: expiresAt,
            jwt: inviteJwt,
            notes: notes,
            redeemedAt: null,
            redeemedBy: null,
          };
          this.invites = [newInvite, ...this.invites];
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
        } catch (error: any) {
          this.lookForErrorAndNotify(
            error,
            "Error Creating Invite",
            "Got an error creating your invite.",
          );
        }
      },
    );
  }

  /**
   * Adds a new contact from redeemed invitation
   *
   * @param did - DID of the person who redeemed the invitation
   * @param notes - Notes from the original invitation
   */
  async addNewContact(did: string, notes: string) {
    (this.$refs.contactNameDialog as ContactNameDialog).open(
      "To Whom Did You Send The Invite?",
      "Their name will be added to your contact list.",
      async (name) => {
        // The person registered and user granted visibility, so add them
        const contact = {
          did: did,
          name: name,
          registered: true,
        };

        // Use PlatformServiceMixin service method for contact insertion
        await this.$insertContact(contact);

        this.contactsRedeemed[did] = contact;
        this.notify.success(
          createContactAddedMessage(name || "Contact"),
          TIMEOUTS.STANDARD,
        );
      },
      () => {},
      notes,
    );
  }

  deleteInvite(inviteId: string, notes: string) {
    this.notify.confirm(createInviteDeleteConfirmMessage(notes), async () => {
      const headers = await getHeaders(this.activeDid);
      try {
        const result = await axios.delete(
          this.apiServer + "/api/userUtil/invite/" + inviteId,
          { headers },
        );
        if (result.status !== 204) {
          throw result.data;
        }
        this.invites = this.invites.filter(
          (invite) => invite.inviteIdentifier !== inviteId,
        );
        this.notify.success(NOTIFY_INVITE_DELETED.message, TIMEOUTS.STANDARD);
      } catch (e) {
        this.lookForErrorAndNotify(
          e,
          "Error Deleting Invite",
          "Got an error deleting your invite.",
        );
      }
    });
  }
}
</script>
