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

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

    <!-- Identity Details -->
    <div
      v-if="!!contactFromDid"
      class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"
    >
      <div>
        <h2 class="text-xl font-semibold">
          {{ contactFromDid?.name || "(no name)" }}
          <router-link
            :to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }"
          >
            <fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
          </router-link>
        </h2>
        <button
          @click="showDidDetails = !showDidDetails"
          class="ml-2 mr-2 mt-4"
        >
          Details
          <fa v-if="showDidDetails" icon="chevron-down" class="text-blue-400" />
          <fa v-else icon="chevron-right" class="text-blue-400" />
        </button>
        <!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
        <pre
          v-if="showDidDetails"
          class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
          >{{ contactYaml }}</pre
        >
      </div>
      <div class="flex justify-center mt-4">
        <span
          v-if="contactFromDid?.profileImageUrl"
          class="flex justify-between"
        >
          <EntityIcon
            :icon-size="96"
            :profileImageUrl="contactFromDid?.profileImageUrl"
            class="inline-block align-text-bottom border border-slate-300 rounded"
            @click="showLargeIdenticonUrl = contactFromDid?.profileImageUrl"
          />
        </span>
      </div>
      <div class="flex justify-between mt-4">
        <div class="flex items-center">
          <div v-if="activeDid" class="flex justify-between">
            <div>
              <button
                v-if="
                  contactFromDid?.seesMe && contactFromDid.did !== activeDid
                "
                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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
                @click="confirmSetVisibility(contactFromDid, false)"
                title="They can see you"
              >
                <fa icon="eye" class="fa-fw" />
              </button>
              <button
                v-else-if="
                  !contactFromDid?.seesMe && contactFromDid?.did !== activeDid
                "
                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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
                @click="confirmSetVisibility(contactFromDid, true)"
                title="They cannot see you"
              >
                <fa icon="eye-slash" class="fa-fw" />
              </button>

              <button
                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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
                @click="checkVisibility(contactFromDid)"
                title="Check Visibility"
                v-if="contactFromDid?.did !== activeDid"
              >
                <fa icon="rotate" class="fa-fw" />
              </button>
            </div>

            <button
              @click="confirmRegister(contactFromDid)"
              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 ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
              v-if="contactFromDid?.did !== activeDid"
              title="Registration"
            >
              <fa
                v-if="contactFromDid?.registered"
                icon="person-circle-check"
                class="fa-fw"
              />
              <fa v-else icon="person-circle-question" class="fa-fw" />
            </button>
          </div>

          <button
            @click="confirmDeleteContact(contactFromDid)"
            class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
            title="Delete"
          >
            <fa icon="trash-can" class="fa-fw" />
          </button>
        </div>
        <div v-if="!contactFromDid?.profileImageUrl">
          <div>Auto-Generated Icon</div>
          <div class="flex justify-center">
            <EntityIcon
              :entityId="viewingDid"
              :iconSize="64"
              class="inline-block align-middle border border-slate-300 rounded-md mr-1"
              @click="showLargeIdenticonId = viewingDid"
            />
          </div>
        </div>
      </div>
      <div
        v-if="showLargeIdenticonId || showLargeIdenticonUrl"
        class="fixed z-[100] top-0 inset-x-0 w-full"
      >
        <div
          class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
        >
          <EntityIcon
            :entityId="showLargeIdenticonId"
            :iconSize="512"
            :profileImageUrl="showLargeIdenticonUrl"
            class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
            @click="
              showLargeIdenticonId = undefined;
              showLargeIdenticonUrl = undefined;
            "
          />
        </div>
      </div>
    </div>
    <div v-else class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
      <!-- !contactFromDid -->
      <div>
        <h2 class="text-xl font-semibold">
          {{ isMyDid ? "You" : "(no name)" }}
        </h2>
      </div>
    </div>

    <!-- Loading Animation -->
    <div
      class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
      v-if="isLoading"
    >
      <fa icon="spinner" class="fa-spin-pulse"></fa>
    </div>
    <!-- Results List -->
    <div v-if="claims.length > 0" class="mt-4">
      <div class="text-l font-bold text-center">
        Claims That Involve {{ isMyDid ? "You" : "Them" }}
      </div>
    </div>
    <InfiniteScroll @reached-bottom="loadMoreData">
      <ul>
        <li
          class="border-b border-slate-300"
          v-for="claim in claims"
          :key="claim.handleId"
        >
          <div class="grid grid-cols-12 gap-4">
            <span class="col-span-2">
              {{ claim.issuedAt.substring(0, 10) }}
            </span>
            <span class="col-span-2">
              {{ capitalizeAndInsertSpacesBeforeCaps(claim.claimType) }}
            </span>
            <span class="col-span-2">
              {{ claimAmount(claim) }}
            </span>
            <span class="col-span-5">
              {{ claimDescription(claim) }}
            </span>
            <span class="col-span-1">
              <a @click="onClickLoadClaim(claim.id)" class="cursor-pointer">
                <fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
              </a>
            </span>
          </div>
        </li>
      </ul>
    </InfiniteScroll>

    <div
      v-if="!isLoading && claims.length === 0"
      class="flex justify-center mt-4"
    >
      <span v-if="isMyDid">You have no claims yet.</span>
      <span v-else>They are in no claims visible to you.</span>
    </div>
  </section>
</template>

<script lang="ts">
import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";

import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { BoundingBox } from "@/db/tables/settings";
import {
  capitalizeAndInsertSpacesBeforeCaps,
  didInfoForContact,
  displayAmount,
  getHeaders,
  GenericCredWrapper,
  GenericVerifiableCredential,
  GiveVerifiableCredential,
  OfferVerifiableCredential,
  register,
  setVisibilityUtil,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import EntityIcon from "@/components/EntityIcon.vue";

@Component({
  components: {
    EntityIcon,
    InfiniteScroll,
    QuickNav,
    TopMessage,
  },
})
export default class DIDView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;

  libsUtil = libsUtil;
  yaml = yaml;

  activeDid = "";
  apiServer = "";
  claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
  contactFromDid?: Contact;
  contactYaml = "";
  hitEnd = false;
  isLoading = false;
  isMyDid = false;
  searchBox: { name: string; bbox: BoundingBox } | null = null;
  showDidDetails = false;
  showLargeIdenticonId?: string;
  showLargeIdenticonUrl?: string;
  viewingDid?: string;

  capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
  didInfoForContact = didInfoForContact;
  displayAmount = displayAmount;

  async mounted() {
    const settings = await retrieveSettingsForActiveAccount();
    this.activeDid = settings.activeDid || "";
    this.apiServer = settings.apiServer || "";

    const pathParam = window.location.pathname.substring("/did/".length);
    if (pathParam) {
      this.viewingDid = decodeURIComponent(pathParam);
      this.contactFromDid = await db.contacts.get(this.viewingDid);
      if (this.contactFromDid) {
        this.contactYaml = yaml.dump(this.contactFromDid);
      }
      await this.loadClaimsAbout();

      const allAccountDids = await libsUtil.retrieveAccountDids();
      this.isMyDid = allAccountDids.includes(this.viewingDid);
    }
  }

  /**
   * Data loader used by infinite scroller
   * @param payload is the flag from the InfiniteScroll indicating if it should load
   **/
  async loadMoreData(payload: boolean) {
    if (this.claims.length > 0 && !this.hitEnd && payload) {
      this.loadClaimsAbout();
    }
  }

  // prompt with confirmation if they want to delete a contact
  confirmDeleteContact(contact: Contact) {
    let message =
      "Are you sure you want to remove " +
      libsUtil.nameForContact(contact, false) +
      " from your contact list?";
    if (contact.seesMe) {
      message +=
        " Note that they can see your activity, so if you want to hide your activity from them then you should do that first.";
    }
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Delete",
        text: message,
        onYes: async () => {
          await this.deleteContact(contact);
        },
      },
      -1,
    );
  }

  async deleteContact(contact: Contact) {
    await db.open();
    await db.contacts.delete(contact.did);
    this.$notify(
      {
        group: "alert",
        type: "success",
        title: "Deleted",
        text: "Contact has been removed.",
      },
      3000,
    );
    (this.$router as Router).push({ name: "contacts" });
  }

  // confirm to register a new contact
  async confirmRegister(contact: Contact) {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Register",
        text:
          "Are you sure you want to register " +
          libsUtil.nameForContact(this.contactFromDid, false) +
          (contact.registered
            ? " -- especially since they are already marked as registered"
            : "") +
          "?",
        onYes: async () => {
          await this.register(contact);
        },
      },
      -1,
    );
  }

  // note that this is also in ContactView.vue
  async register(contact: Contact) {
    this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);

    try {
      const regResult = await register(
        this.activeDid,
        this.apiServer,
        this.axios,
        contact,
      );
      if (regResult.success) {
        contact.registered = true;
        await db.contacts.update(contact.did, { registered: true });

        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Registration Success",
            text:
              (contact.name || "That unnamed person") + " has been registered.",
          },
          5000,
        );
      } else {
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Registration Error",
            text:
              (regResult.error as string) ||
              "Something went wrong during registration.",
          },
          5000,
        );
      }
    } catch (error) {
      console.error("Error when registering:", error);
      let userMessage = "There was an error.";
      const serverError = error as AxiosError;
      if (serverError) {
        if (serverError.response?.data?.error?.message) {
          userMessage = serverError.response.data.error.message;
        } 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(
        {
          group: "alert",
          type: "danger",
          title: "Registration Error",
          text: userMessage,
        },
        5000,
      );
    }
  }

  public async loadClaimsAbout() {
    if (!this.viewingDid) {
      console.error("This should never be called without a DID.");
      return;
    }

    const queryParams = "claimContents=" + encodeURIComponent(this.viewingDid);
    let postfix = "";
    if (this.claims.length > 0) {
      postfix = "&beforeId=" + this.claims[this.claims.length - 1].id;
    }

    try {
      this.isLoading = true;
      const response = await fetch(
        this.apiServer + "/api/v2/report/claims?" + queryParams + postfix,
        {
          method: "GET",
          headers: await getHeaders(this.activeDid),
        },
      );

      if (response.status !== 200) {
        const details = await response.text();
        console.error("Problem with full search:", details);
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error",
            text: `There was a problem accessing the server. Try again later.`,
          },
          5000,
        );
        return;
      }

      const results = await response.json();
      this.claims = this.claims.concat(results.data);
      this.hitEnd = !results.hitLimit;

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (e: any) {
      console.error("Error with feed load:", e);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: e.userMessage || "There was a problem retrieving claims.",
        },
        -1,
      );
    } finally {
      this.isLoading = false;
    }
  }

  onClickLoadClaim(jwtId: string) {
    const route = {
      path: "/claim/" + encodeURIComponent(jwtId),
    };
    (this.$router as Router).push(route);
  }

  public claimAmount(claim: GenericVerifiableCredential) {
    if (claim.claimType === "GiveAction") {
      const giveClaim = claim.claim as GiveVerifiableCredential;
      if (giveClaim.object?.unitCode && giveClaim.object?.amountOfThisGood) {
        return displayAmount(
          giveClaim.object.unitCode,
          giveClaim.object.amountOfThisGood,
        );
      } else {
        return "";
      }
    } else if (claim.claimType === "Offer") {
      const offerClaim = claim.claim as OfferVerifiableCredential;
      if (
        offerClaim.includesObject?.unitCode &&
        offerClaim.includesObject?.amountOfThisGood
      ) {
        return displayAmount(
          offerClaim.includesObject.unitCode,
          offerClaim.includesObject.amountOfThisGood,
        );
      } else {
        return "";
      }
    }
    return "";
  }

  claimDescription(claim: GenericVerifiableCredential) {
    return claim.claim.name || claim.claim.description || "";
  }

  // note that this is also in ContactView.vue
  async confirmSetVisibility(contact: Contact, visibility: boolean) {
    const visibilityPrompt = visibility
      ? "Are you sure you want to make your activity visible to them?"
      : "Are you sure you want to hide all your activity from them?";
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Set Visibility",
        text: visibilityPrompt,
        onYes: async () => {
          const success = await this.setVisibility(contact, visibility, true);
          if (success) {
            contact.seesMe = visibility; // didn't work inside setVisibility
          }
        },
      },
      -1,
    );
  }

  // note that this is also in ContactView.vue
  async setVisibility(
    contact: Contact,
    visibility: boolean,
    showSuccessAlert: boolean,
  ) {
    const result = await setVisibilityUtil(
      this.activeDid,
      this.apiServer,
      this.axios,
      db,
      contact,
      visibility,
    );
    if (result.success) {
      //contact.seesMe = visibility; // why doesn't it affect the UI from here?
      //console.log("Set result & seesMe", result, contact.seesMe, contact.did);
      if (showSuccessAlert) {
        this.$notify(
          {
            group: "alert",
            type: "success",
            title: "Visibility Set",
            text:
              (contact.name || "That user") +
              " can " +
              (visibility ? "" : "not ") +
              "see your activity.",
          },
          3000,
        );
      }
      return true;
    } else {
      console.error("Got strange result from setting visibility:", result);
      const message =
        (result.error as string) || "Could not set visibility on the server.";
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Setting Visibility",
          text: message,
        },
        5000,
      );
      return false;
    }
  }

  // note that this is also in ContactView.vue
  async checkVisibility(contact: Contact) {
    const url =
      this.apiServer +
      "/api/report/canDidExplicitlySeeMe?did=" +
      encodeURIComponent(contact.did);
    const headers = await getHeaders(this.activeDid);
    if (!headers["Authorization"]) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "No Identity",
          text: "There is no identity to use to check visibility.",
        },
        3000,
      );
      return;
    }

    try {
      const resp = await this.axios.get(url, { headers });
      if (resp.status === 200) {
        const visibility = resp.data;
        contact.seesMe = visibility;
        //console.log("Visi check:", visibility, contact.seesMe, contact.did);
        await db.contacts.update(contact.did, { seesMe: visibility });

        this.$notify(
          {
            group: "alert",
            type: "info",
            title: "Visibility Refreshed",
            text:
              libsUtil.nameForContact(contact, true) +
              " can " +
              (visibility ? "" : "not ") +
              "see your activity.",
          },
          3000,
        );
      } else {
        console.error("Got bad server response checking visibility:", resp);
        const message = resp.data.error?.message || "Got bad server response.";
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error Checking Visibility",
            text: message,
          },
          5000,
        );
      }
    } catch (err) {
      console.error("Caught error from request to check visibility:", err);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Checking Visibility",
          text: "Check connectivity and try again.",
        },
        3000,
      );
    }
  }
}
</script>

<style>
.dialog-overlay {
  z-index: 50;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 1.5rem;
}
.dialog {
  background-color: white;
  padding: 1rem;
  border-radius: 0.5rem;
  width: 100%;
  max-width: 500px;
}
</style>
