<template>
  <QuickNav selected="Profile"></QuickNav>
  <!-- CONTENT -->
  <section id="Content" class="p-6 pb-24">
    <!-- Heading -->
    <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
      Your Identity
    </h1>

    <div class="flex justify-between">
      <span />
      <span class="whitespace-nowrap">
        <router-link
          :to="{ name: 'contact-qr' }"
          class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md"
        >
          <fa icon="qrcode" class="fa-fw"></fa>
        </router-link>
      </span>
      <span />
    </div>

    <div class="flex justify-between py-2">
      <span />
      <span>
        <router-link
          :to="{ name: 'help' }"
          class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
        >
          Help
        </router-link>
      </span>
    </div>

    <!-- Registration notice -->
    <!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
    <div
      v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
      class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
    >
      <p class="mb-4">
        <b>Note:</b> Before you can publicly announce a new project or time
        commitment, a friend needs to register you.
      </p>
      <router-link
        :to="{ name: 'contact-qr' }"
        class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
      >
        Share Your Info
      </router-link>
    </div>

    <!-- Identity Details -->
    <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
      <h2 v-if="givenName" class="text-xl font-semibold mb-2">
        {{ givenName }}
      </h2>
      <span v-else>
        <router-link
          :to="{ name: 'new-edit-account' }"
          class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
        >
          (set name)
        </router-link>
      </span>

      <div class="text-slate-500 text-sm font-bold">ID</div>
      <div class="text-sm text-slate-500 flex justify-start items-center mb-1">
        <code class="truncate">{{ activeDid }}</code>
        <button
          @click="
            doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
          "
          class="ml-2"
        >
          <fa icon="copy" class="text-slate-400 fa-fw"></fa>
        </button>
        <span v-show="showDidCopy">Copied!</span>
      </div>
    </div>

    <router-link
      :to="{ name: 'new-edit-account' }"
      class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2"
    >
      Edit Identity
    </router-link>

    <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
      <label
        for="toggleNotifications"
        class="flex items-center cursor-pointer"
        @click="
          this.$notify(
            {
              group: 'modal',
              type: 'notification-permission',
            },
            -1,
          )
        "
      >
        <!-- label -->
        <div>App Notifications</div>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input type="checkbox" name="toggleNotifications" class="sr-only" />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          ></div>
        </div>
      </label>
      <label
        for="toggleMuteNotifications"
        class="flex items-center cursor-pointer mt-4"
        @click="
          this.$notify(
            {
              group: 'modal',
              type: 'notification-mute',
            },
            -1,
          )
        "
      >
        <!-- label -->
        <div>Mute Notifications</div>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input
            type="checkbox"
            name="toggleMuteNotifications"
            class="sr-only"
          />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          ></div>
        </div>
      </label>
    </div>

    <h3 class="text-sm uppercase font-semibold mb-3">Data</h3>

    <router-link
      :to="{ name: 'seed-backup' }"
      href=""
      class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
    >
      Backup Identifier Seed
    </router-link>
    <a
      class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
      @click="exportDatabase()"
    >
      Download Settings & Contacts
      <br />
      (excluding Identifier Data)
    </a>
    <a ref="downloadLink" />

    <div v-if="activeDid" class="flex py-2">
      <button class="text-center text-md text-blue-500" @click="checkLimits()">
        Check Limits
      </button>
      <!-- show spinner if loading limits -->
      <div v-if="loadingLimits" class="ml-2">
        Checking... <fa icon="spinner" class="fa-spin"></fa>
      </div>
      <div class="ml-2">
        {{ limitsMessage }}
      </div>
      <div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
        <span class="font-bold">Rate Limits</span>
        <p>
          You have done {{ limits.doneClaimsThisWeek }} claims out of
          {{ limits.maxClaimsPerWeek }} for this week. Your claims counter
          resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
        </p>
        <p>
          You have done {{ limits.doneRegistrationsThisMonth }} registrations
          out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
          registrations counter resets at
          {{ readableTime(limits.nextMonthBeginDateTime) }}
        </p>
      </div>
    </div>

    <!-- id used by puppeteer test script -->
    <h3
      id="advanced"
      class="text-sm uppercase font-semibold mb-3"
      @click="showAdvanced = !showAdvanced"
    >
      Advanced
    </h3>

    <div v-if="showAdvanced">
      <!-- Deep Identity Details -->
      <h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
        Deep Identity Details
      </h2>
      <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
        <div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
        <div
          class="text-sm text-slate-500 flex justify-start items-center mb-1"
        >
          <code class="truncate">{{ publicBase64 }}</code>
          <button
            @click="
              doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
            "
            class="ml-2"
          >
            <fa icon="copy" class="text-slate-400 fa-fw"></fa>
          </button>
          <span v-show="showB64Copy">Copied!</span>
        </div>

        <div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
        <div
          class="text-sm text-slate-500 flex justify-start items-center mb-1"
        >
          <code class="truncate">{{ publicHex }}</code>
          <button
            @click="
              doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
            "
            class="ml-2"
          >
            <fa icon="copy" class="text-slate-400 fa-fw"></fa>
          </button>
          <span v-show="showPubCopy">Copied!</span>
        </div>

        <div class="text-slate-500 text-sm font-bold">Derivation Path</div>
        <div
          class="text-sm text-slate-500 flex justify-start items-center mb-1"
        >
          <code class="truncate">{{ derivationPath }}</code>
          <button
            @click="
              doCopyTwoSecRedo(
                derivationPath,
                () => (showDerCopy = !showDerCopy),
              )
            "
            class="ml-2"
          >
            <fa icon="copy" class="text-slate-400 fa-fw"></fa>
          </button>
          <span v-show="showDerCopy">Copied!</span>
        </div>
      </div>

      <label
        for="toggleShowAmounts"
        class="flex items-center cursor-pointer py-2"
        @click="handleChange"
      >
        <!-- label -->
        <h2 class="text-slate-500 text-sm font-bold mb-2">
          Show amounts given with contacts
        </h2>
        <!-- toggle -->
        <div class="relative ml-2">
          <!-- input -->
          <input
            type="checkbox"
            v-model="showContactGives"
            name="showContactGives"
            class="sr-only"
          />
          <!-- line -->
          <div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
          <!-- dot -->
          <div
            class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
          ></div>
        </div>
      </label>

      <div class="flex py-2">
        <button class="text-blue-500">
          <!-- id used by puppeteer test script -->
          <router-link
            id="switch-identity-link"
            :to="{ name: 'identity-switcher' }"
            class="block text-center"
          >
            Switch Identity / No Identity
          </router-link>
        </button>
      </div>

      <div class="flex py-2">
        <button class="text-blue-500">
          <router-link :to="{ name: 'statistics' }" class="block text-center">
            See Achievements & Statistics
          </router-link>
        </button>
      </div>

      <div class="flex py-4">
        <h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
        <input
          type="text"
          class="block w-full rounded border border-slate-400 px-3 py-2"
          v-model="apiServerInput"
        />
        <button
          v-if="apiServerInput != apiServer"
          class="px-4 rounded bg-red-500 border border-slate-400"
          @click="onClickSaveApiServer()"
        >
          <fa icon="floppy-disk" class="fa-fw" color="white"></fa>
        </button>
        <button
          class="px-4 rounded bg-slate-200 border border-slate-400"
          @click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
        >
          Use Prod
        </button>
        <button
          class="px-4 rounded bg-slate-200 border border-slate-400"
          @click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
        >
          Use Test
        </button>
        <button
          class="px-4 rounded bg-slate-200 border border-slate-400"
          @click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
        >
          Use Local
        </button>
      </div>

      <div class="flex py-4">
        <h2 class="text-slate-500 text-sm font-bold mb-2">
          Notification Push Server
        </h2>
        <input
          type="text"
          class="block w-full rounded border border-slate-400 px-3 py-2"
          v-model="webPushServerInput"
        />
        <button
          v-if="webPushServerInput != webPushServer"
          class="px-4 rounded bg-red-500 border border-slate-400"
          @click="onClickSavePushServer()"
        >
          <fa icon="floppy-disk" class="fa-fw" color="white"></fa>
        </button>
        <button
          class="px-4 rounded bg-slate-200 border border-slate-400"
          @click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
        >
          Use Prod
        </button>
        <button
          class="px-4 rounded bg-slate-200 border border-slate-400"
          @click="webPushServerInput = AppConstants.TEST_PUSH_SERVER"
        >
          Use Test
        </button>
        <button
          class="px-4 rounded bg-slate-200 border border-slate-400"
          @click="webPushServerInput = AppConstants.LOCAL_PUSH_SERVER"
        >
          Use Local
        </button>
      </div>
    </div>
  </section>
</template>

<script lang="ts">
import { AxiosError, AxiosRequestConfig } from "axios";
import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";

import QuickNav from "@/components/QuickNav.vue";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;

interface Notification {
  group: string;
  type: string;
  title: string;
  text: string;
}

interface IAccount {
  did: string;
  publicKeyHex: string;
  privateHex?: string;
  derivationPath: string;
}

@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue {
  $notify!: (notification: Notification, timeout?: number) => void;

  AppConstants = AppString;

  activeDid = "";
  apiServer = "";
  apiServerInput = "";
  derivationPath = "";
  givenName = "";
  isRegistered = false;
  numAccounts = 0;
  publicHex = "";
  publicBase64 = "";
  webPushServer = "";
  webPushServerInput = "";
  limits: RateLimits | null = null;
  limitsMessage = "";
  loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
  showContactGives = false;

  showDidCopy = false;
  showDerCopy = false;
  showB64Copy = false;
  showPubCopy = false;

  showAdvanced = false;

  public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
    try {
      // Open the accounts database
      await accountsDB.open();
    } catch (error) {
      console.error("Failed to open accounts database:", error);
      return null;
    }

    let account: { identity?: string } | undefined;

    try {
      // Search for the account with the matching DID (decentralized identifier)
      account = await accountsDB.accounts
        .where("did")
        .equals(activeDid)
        .first();
    } catch (error) {
      console.error("Failed to find account:", error);
      return null;
    }

    // Return parsed identity or null if not found
    return JSON.parse((account?.identity as string) || "null");
  }

  /**
   * Asynchronously retrieves headers for HTTP requests.
   *
   * @param {IIdentifier} identity - The identity object for which to generate the headers.
   * @returns {Promise<Record<string,string>>} A Promise that resolves to an object containing the headers.
   *
   * @throws Will throw an error if unable to generate an access token.
   */
  public async getHeaders(
    identity: IIdentifier,
  ): Promise<Record<string, string>> {
    try {
      const token = await accessToken(identity);

      const headers: Record<string, string> = {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      };

      return headers;
    } catch (error) {
      console.error("Failed to get headers:", error);
      return Promise.reject(error);
    }
  }

  // call fn, copy text to the clipboard, then redo fn after 2 seconds
  doCopyTwoSecRedo(text: string, fn: () => void) {
    fn();
    useClipboard()
      .copy(text)
      .then(() => setTimeout(fn, 2000));
  }

  handleChange() {
    this.showContactGives = !this.showContactGives;
    this.updateShowContactAmounts();
  }

  readableTime(timeStr: string) {
    return timeStr.substring(0, timeStr.indexOf("T"));
  }

  async beforeCreate() {
    await accountsDB.open();
    this.numAccounts = await accountsDB.accounts.count();
  }

  /**
   * Async function executed when the component is created.
   * Initializes the component's state with values from the database,
   * handles identity-related tasks, and checks limitations.
   *
   * @throws Will display specific messages to the user based on different errors.
   */
  async created() {
    try {
      await db.open();

      const settings = await db.settings.get(MASTER_SETTINGS_KEY);

      // Initialize component state with values from the database or defaults
      this.initializeState(settings);

      // Get and process the identity
      const identity = await this.getIdentity(this.activeDid);
      if (identity) {
        this.processIdentity(identity);
      }
    } catch (err: unknown) {
      this.handleError(err);
    }
  }

  /**
   * Initializes component state with values from the database or defaults.
   * @param {SettingsType} settings - Object containing settings from the database.
   */
  initializeState(settings: Settings | undefined) {
    this.activeDid = (settings?.activeDid as string) || "";
    this.apiServer = (settings?.apiServer as string) || "";
    this.apiServerInput = (settings?.apiServer as string) || "";
    this.givenName =
      (settings?.firstName || "") +
      (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
    this.isRegistered = !!settings?.isRegistered;
    this.webPushServer = (settings?.webPushServer as string) || "";
    this.webPushServerInput = (settings?.webPushServer as string) || "";
    this.showContactGives = !!settings?.showContactGivesInline;
  }

  /**
   * Processes the identity and updates the component's state.
   * @param {IdentityType} identity - Object containing identity information.
   */
  processIdentity(identity: IIdentifier) {
    if (
      identity &&
      identity.keys &&
      identity.keys.length > 0 &&
      identity.keys[0].meta
    ) {
      this.publicHex = identity.keys[0].publicKeyHex;
      this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
      this.derivationPath = identity.keys[0].meta.derivationPath as string;

      db.settings.update(MASTER_SETTINGS_KEY, {
        activeDid: identity.did,
      });
      this.checkLimitsFor(identity);
    } else {
      // Handle the case where any of these are null or undefined
    }
  }

  /**
   * Handles errors and updates the component's state accordingly.
   * @param {Error} err - The error object.
   */
  handleError(err: unknown) {
    if (
      err instanceof Error &&
      err.message ===
        "Attempted to load account records with no identity available."
    ) {
      this.limitsMessage = "No identity.";
      this.loadingLimits = false;
    } else {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Creating Account",
          text: "Clear your cache and start over (after data backup).",
        },
        -1,
      );
      console.error("Telling user to clear cache at page create because:", err);
    }
  }

  public async updateShowContactAmounts() {
    try {
      await db.open();
      db.settings.update(MASTER_SETTINGS_KEY, {
        showContactGivesInline: this.showContactGives,
      });
    } catch (err) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Updating Contact Setting",
          text: "Clear your cache and start over (after data backup).",
        },
        -1,
      );
      console.error(
        "Telling user to clear cache after contact setting update because:",
        err,
      );
    }
  }

  /**
   * Asynchronously exports the database into a downloadable JSON file.
   *
   * @throws Will notify the user if there is an export error.
   */
  public async exportDatabase() {
    try {
      // Generate the blob from the database
      const blob = await this.generateDatabaseBlob();

      // Create a temporary URL for the blob
      const url = this.createBlobURL(blob);

      // Trigger the download
      this.downloadDatabaseBackup(url);

      // Revoke the temporary URL
      URL.revokeObjectURL(url);

      // Notify the user that the download has started
      this.notifyDownloadStarted();
    } catch (error) {
      this.handleExportError(error);
    }
  }

  /**
   * Generates a blob object representing the database.
   *
   * @returns {Promise<Blob>} The generated blob object.
   */
  private async generateDatabaseBlob(): Promise<Blob> {
    return await db.export({ prettyJson: true });
  }

  /**
   * Creates a temporary URL for a blob object.
   *
   * @param {Blob} blob - The blob object.
   * @returns {string} The temporary URL for the blob.
   */
  private createBlobURL(blob: Blob): string {
    return URL.createObjectURL(blob);
  }

  /**
   * Triggers the download of the database backup.
   *
   * @param {string} url - The temporary URL for the blob.
   */
  private downloadDatabaseBackup(url: string) {
    const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
    downloadAnchor.href = url;
    downloadAnchor.download = `${db.name}-backup.json`;
    downloadAnchor.click();
  }

  /**
   * Notifies the user that the download has started.
   */
  private notifyDownloadStarted() {
    this.$notify(
      {
        group: "alert",
        type: "toast",
        title: "Download Started",
        text: "See your downloads directory for the backup.",
      },
      5000,
    );
  }

  /**
   * Handles errors during the database export process.
   *
   * @param {Error} error - The error object.
   */
  private handleExportError(error: unknown) {
    this.$notify(
      {
        group: "alert",
        type: "danger",
        title: "Export Error",
        text: "See console logs for more info.",
      },
      -1,
    );
    console.error("Export Error:", error);
  }

  async checkLimits() {
    const identity = await this.getIdentity(this.activeDid);
    if (identity) {
      this.checkLimitsFor(identity);
    }
  }

  /**
   * Asynchronously checks rate limits for the given identity.
   *
   * Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
   */
  public async checkLimitsFor(identity: IIdentifier) {
    this.loadingLimits = true;
    this.limitsMessage = "";

    try {
      const resp = await this.fetchRateLimits(identity);
      if (resp.status === 200) {
        this.limits = resp.data;
        if (!this.isRegistered) {
          // the user is not known to be registered, but they are so let's record it
          try {
            await db.open();
            db.settings.update(MASTER_SETTINGS_KEY, {
              isRegistered: true,
            });
            this.isRegistered = true;
          } catch (err) {
            console.log("Got an error updating settings:", err);
            this.$notify(
              {
                group: "alert",
                type: "warning",
                title: "Update Error",
                text: "Unable to update your settings. Check claim limits again.",
              },
              -1,
            );
          }
        }
      }
    } catch (error) {
      this.handleRateLimitsError(error);
    }

    this.loadingLimits = false;
  }

  /**
   * Fetches rate limits from the server.
   *
   * @param {IIdentifier} identity - The identity object to check rate limits for.
   * @returns {Promise<AxiosResponse>} The Axios response object.
   */
  private async fetchRateLimits(identity: IIdentifier) {
    const url = `${this.apiServer}/api/report/rateLimits`;
    const headers = await this.getHeaders(identity);
    return await this.axios.get(url, { headers } as AxiosRequestConfig);
  }

  /**
   * Handles errors that occur while fetching rate limits.
   *
   * @param {AxiosError | Error} error - The error object.
   */
  private handleRateLimitsError(error: unknown) {
    if (error instanceof AxiosError) {
      const data = error.response?.data as ErrorResponse;
      this.limitsMessage =
        (data?.error?.message as string) || "Bad server response.";
      console.log(
        "Got bad response retrieving limits, which usually means user isn't registered. Server says:",
        this.limitsMessage,
        //error,
      );
    } else if (
      error instanceof Error &&
      error.message ===
        "Attempted to load Give records with no identity available."
    ) {
      this.limitsMessage = "No identity.";
    } else {
      // Handle other unknown errors
    }
  }

  /**
   * Asynchronously switches the active account based on the provided account number.
   *
   * @param {number} accountNum - The account number to switch to. 0 means none.
   */
  public async switchAccount(accountNum: number) {
    await db.open(); // Assumes db needs to be open for both cases

    if (accountNum === 0) {
      this.switchToNoAccount();
    } else {
      await this.switchToAccountNumber(accountNum);
    }
  }

  /**
   * Switches to no active account and clears relevant properties.
   */
  private async switchToNoAccount() {
    await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: undefined });
    this.clearActiveAccountProperties();
  }

  /**
   * Clears properties related to the active account.
   */
  private clearActiveAccountProperties() {
    this.activeDid = "";
    this.derivationPath = "";
    this.publicHex = "";
    this.publicBase64 = "";
  }

  /**
   * Switches to an account based on its number in the list.
   *
   * @param {number} accountNum - The account number to switch to.
   */
  private async switchToAccountNumber(accountNum: number) {
    await accountsDB.open();
    const accounts = await accountsDB.accounts.toArray();
    const account = accounts[accountNum - 1];

    await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });

    this.updateActiveAccountProperties(account);
  }

  /**
   * Updates properties related to the active account.
   *
   * @param {AccountType} account - The account object.
   */
  private updateActiveAccountProperties(account: IAccount) {
    this.activeDid = account.did;
    this.derivationPath = account.derivationPath;
    this.publicHex = account.publicKeyHex;
    this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
  }

  public showContactGivesClassNames() {
    return {
      "bg-slate-900": !this.showContactGives,
      "bg-green-600": this.showContactGives,
    };
  }

  async onClickSaveApiServer() {
    await db.open();
    db.settings.update(MASTER_SETTINGS_KEY, {
      apiServer: this.apiServerInput,
    });
    this.apiServer = this.apiServerInput;
  }

  async onClickSavePushServer() {
    await db.open();
    db.settings.update(MASTER_SETTINGS_KEY, {
      webPushServer: this.webPushServerInput,
    });
    this.webPushServer = this.webPushServerInput;
  }
}
</script>
