<template>
  <QuickNav selected="Home"></QuickNav>
  <!-- CONTENT -->
  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
    <!-- Sub View Heading -->
    <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
      <h1 class="grow text-xl text-center font-semibold leading-tight">
        New Activity For You
      </h1>

      <!-- Back -->
      <a
        class="order-first text-lg text-center leading-none p-1"
        @click="$router.go(-1)"
      >
        <font-awesome icon="chevron-left" class="block text-center w-[1em]" />
      </a>

      <!-- Help button -->
      <router-link
        :to="{ name: 'help' }"
        class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
      >
        <font-awesome icon="question" class="block text-center w-[1em]" />
      </router-link>
    </div>

    <!-- Display a single row with the name of "New Offers To You" with a count. -->
    <div class="flex justify-between" data-testId="showOffersToUser">
      <div>
        <span class="text-lg font-medium"
          >{{ newOffersToUser.length
          }}{{ newOffersToUserHitLimit ? "+" : "" }}</span
        >
        <span class="text-lg font-medium ml-4"
          >New Offer{{ newOffersToUser.length === 1 ? "" : "s" }} To You</span
        >
        <font-awesome
          v-if="newOffersToUser.length > 0"
          :icon="showOffersDetails ? 'chevron-down' : 'chevron-right'"
          class="cursor-pointer ml-4 mr-4 text-lg"
          @click.prevent="expandOffersToUserAndMarkRead()"
        />
      </div>
      <a class="text-blue-500 cursor-pointer" @click="handleSeeAllOffersToUser">
        See&nbsp;all
      </a>
    </div>

    <div v-if="showOffersDetails" class="ml-4 mt-4">
      <ul class="list-disc ml-4">
        <li
          v-for="offer in newOffersToUser"
          :key="offer.jwtId"
          class="mt-4 relative group"
        >
          <span>{{
            didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
          }}</span>
          offered
          <span v-if="offer.objectDescription" class="truncate">{{
            offer.objectDescription
          }}</span
          >{{ offer.objectDescription && offer.amount ? ", and " : "" }}
          <span v-if="offer.amount">{{
            displayAmount(offer.unit, offer.amount)
          }}</span>
          <router-link
            :to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
            class="text-blue-500"
          >
            <font-awesome
              icon="file-lines"
              class="pl-2 text-blue-500 cursor-pointer"
            />
          </router-link>
          <!-- New line that appears on hover or when the offer is clicked -->
          <div
            class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
            @click.prevent="markOffersAsReadStartingWith(offer.jwtId)"
          >
            <span class="inline-block w-8 h-px bg-gray-500 mr-2" />
            Click to keep all above as unread offers
          </div>
        </li>
      </ul>
    </div>

    <!-- Display a single row with the name of "New Offers To Your Projects" with a count. -->
    <div
      class="mt-4 flex justify-between"
      data-testId="showOffersToUserProjects"
    >
      <div>
        <span class="text-lg font-medium"
          >{{ newOffersToUserProjects.length
          }}{{ newOffersToUserProjectsHitLimit ? "+" : "" }}</span
        >
        <span class="text-lg font-medium ml-4"
          >New Offer{{ newOffersToUserProjects.length === 1 ? "" : "s" }} To
          Your Projects</span
        >
        <font-awesome
          v-if="newOffersToUserProjects.length > 0"
          :icon="
            showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
          "
          class="cursor-pointer ml-4 mr-4 text-lg"
          @click.prevent="expandOffersToUserProjectsAndMarkRead()"
        />
      </div>
      <a
        class="text-blue-500 cursor-pointer"
        @click="handleSeeAllOffersToUserProjects"
      >
        See&nbsp;all
      </a>
    </div>

    <div v-if="showOffersToUserProjectsDetails" class="ml-4 mt-4">
      <ul class="list-disc ml-4">
        <li
          v-for="offer in newOffersToUserProjects"
          :key="offer.jwtId"
          class="mt-4 relative group"
        >
          <span>{{
            didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
          }}</span>
          offered
          <span v-if="offer.objectDescription" class="truncate">{{
            offer.objectDescription
          }}</span
          >{{ offer.objectDescription && offer.amount ? ", and " : "" }}
          <span v-if="offer.amount">{{
            displayAmount(offer.unit, offer.amount)
          }}</span>
          to
          <span>{{ offer.planName }}</span>
          <router-link
            :to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
            class="text-blue-500"
          >
            <font-awesome
              icon="file-lines"
              class="pl-2 text-blue-500 cursor-pointer"
            />
          </router-link>
          <!-- New line that appears on hover -->
          <div
            class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
            @click.prevent="
              markOffersToUserProjectsAsReadStartingWith(offer.jwtId)
            "
          >
            <span class="inline-block w-8 h-px bg-gray-500 mr-2" />
            Click to keep all above as unread offers
          </div>
        </li>
      </ul>
    </div>

    <!-- Starred Projects with Changes Section -->
    <div
      class="flex justify-between mt-6"
      data-testId="showStarredProjectChanges"
    >
      <div>
        <span class="text-lg font-medium"
          >{{ newStarredProjectChanges.length
          }}{{ newStarredProjectChangesHitLimit ? "+" : "" }}</span
        >
        <span class="text-lg font-medium ml-4"
          >Starred Project{{
            newStarredProjectChanges.length === 1 ? "" : "s"
          }}
          With Changes</span
        >
        <font-awesome
          v-if="newStarredProjectChanges.length > 0"
          :icon="
            showStarredProjectChangesDetails ? 'chevron-down' : 'chevron-right'
          "
          class="cursor-pointer ml-4 mr-4 text-lg"
          @click.prevent="expandStarredProjectChangesAndMarkRead()"
        />
      </div>
    </div>

    <div v-if="showStarredProjectChangesDetails" class="ml-4 mt-4">
      <ul class="list-disc ml-4">
        <li
          v-for="projectChange in newStarredProjectChanges"
          :key="projectChange.plan.handleId"
          class="mt-4 relative group"
        >
          <div class="flex items-center gap-2">
            <div class="flex-1 min-w-0">
              <span class="font-medium">{{
                projectChange.plan.name || "Unnamed Project"
              }}</span>
              <span
                v-if="projectChange.plan.description"
                class="text-gray-600 block truncate"
              >
                {{ projectChange.plan.description }}
              </span>
            </div>
            <router-link
              :to="{
                path:
                  '/project/' + encodeURIComponent(projectChange.plan.handleId),
              }"
              class="text-blue-500 flex-shrink-0"
            >
              <font-awesome
                icon="file-lines"
                class="text-blue-500 cursor-pointer"
              />
            </router-link>
          </div>
          <!-- Show what changed -->
          <div
            v-if="getPlanDifferences(projectChange.plan.handleId)"
            class="text-sm mt-2"
          >
            <div class="font-medium mb-2">Changes</div>
            <div class="overflow-x-auto">
              <table
                class="w-full text-xs border-collapse border border-gray-300 rounded-lg shadow-sm bg-white"
              >
                <thead>
                  <tr class="bg-gray-50">
                    <th
                      class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
                    ></th>
                    <th
                      class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
                    >
                      Previous
                    </th>
                    <th
                      class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
                    >
                      Current
                    </th>
                  </tr>
                </thead>
                <tbody>
                  <tr
                    v-for="(difference, field) in getPlanDifferences(
                      projectChange.plan.handleId,
                    )"
                    :key="field"
                    class="hover:bg-gray-50"
                  >
                    <td
                      class="border border-gray-300 px-3 py-2 font-medium text-gray-800 break-words"
                    >
                      {{ getDisplayFieldName(field) }}
                    </td>
                    <template v-if="field === 'description'">
                      <td
                        colspan="2"
                        class="border border-gray-300 px-3 py-2 break-words align-top"
                      >
                        <div class="w-3/5 mx-auto">
                          <div
                            v-for="(change, index) in getDescriptionDiff(
                              difference.old,
                              difference.new,
                            )"
                            :key="index"
                          >
                            <vue-markdown
                              :source="change.value"
                              :style="getDiffStyle(change)"
                              class="markdown-content inline-block"
                            />
                          </div>
                        </div>
                      </td>
                    </template>
                    <template v-else>
                      <td
                        class="border border-gray-300 px-3 py-2 text-gray-600 break-words align-top"
                      >
                        <span>{{ formatFieldValue(difference.old) }}</span>
                      </td>
                      <td
                        class="border border-gray-300 px-3 py-2 text-green-700 font-medium break-words align-top"
                      >
                        <span>{{ formatFieldValue(difference.new) }}</span>
                      </td>
                    </template>
                  </tr>
                </tbody>
              </table>
            </div>
          </div>
          <div v-else>
            The changes are not important, like it was saved by accident or
            you've seen it all before.
          </div>
          <!-- New line that appears on hover -->
          <div
            class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
            @click.prevent="
              markStarredProjectChangesAsReadStartingWith(
                projectChange.plan.jwtId!,
              )
            "
          >
            <span class="inline-block w-8 h-px bg-gray-500 mr-2" />
            Click to keep all above as unread changes
          </div>
        </li>
      </ul>
    </div>
  </section>
</template>

<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import VueMarkdown from "vue-markdown-render";
import { diffLines, Change } from "diff";

import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue";
import { NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { Router } from "vue-router";
import {
  OfferSummaryRecord,
  OfferToPlanSummaryRecord,
  PlanSummaryAndPreviousClaim,
  PlanSummaryRecord,
} from "../interfaces/records";
import {
  didInfo,
  didInfoOrNobody,
  displayAmount,
  getNewOffersToUser,
  getNewOffersToUserProjects,
  getStarredProjectsWithChanges,
} from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import * as databaseUtil from "../db/databaseUtil";
import * as R from "ramda";
import { PlanActionClaim } from "../interfaces/claims";
import { GenericCredWrapper } from "@/interfaces";

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

  notify!: ReturnType<typeof createNotifyHelpers>;
  activeDid = "";
  allContacts: Array<Contact> = [];
  allMyDids: string[] = [];
  apiServer = "";
  lastAckedOfferToUserJwtId = "";
  lastAckedOfferToUserProjectsJwtId = "";
  lastAckedStarredPlanChangesJwtId = "";
  newOffersToUser: Array<OfferSummaryRecord> = [];
  newOffersToUserHitLimit = false;
  newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = [];
  newOffersToUserProjectsHitLimit = false;
  newStarredProjectChanges: Array<PlanSummaryAndPreviousClaim> = [];
  newStarredProjectChangesHitLimit = false;
  starredPlanHandleIds: Array<string> = [];
  planDifferences: Record<
    string,
    Record<string, { old: unknown; new: unknown }>
  > = {};

  showOffersDetails = false;
  showOffersToUserProjectsDetails = false;
  showStarredProjectChangesDetails = false;
  didInfo = didInfo;
  displayAmount = displayAmount;

  async created() {
    this.notify = createNotifyHelpers(this.$notify);

    try {
      const settings = await this.$accountSettings();
      this.apiServer = settings.apiServer || "";

      // 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.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
      this.lastAckedOfferToUserProjectsJwtId =
        settings.lastAckedOfferToUserProjectsJwtId || "";
      this.lastAckedStarredPlanChangesJwtId =
        settings.lastAckedStarredPlanChangesJwtId || "";
      this.starredPlanHandleIds = databaseUtil.parseJsonField(
        settings.starredPlanHandleIds,
        [],
      );

      this.allContacts = await this.$getAllContacts();

      this.allMyDids = await retrieveAccountDids();

      const offersToUserData = await getNewOffersToUser(
        this.axios,
        this.apiServer,
        this.activeDid,
        this.lastAckedOfferToUserJwtId,
      );
      this.newOffersToUser = offersToUserData.data;
      this.newOffersToUserHitLimit = offersToUserData.hitLimit;

      const offersToUserProjectsData = await getNewOffersToUserProjects(
        this.axios,
        this.apiServer,
        this.activeDid,
        this.lastAckedOfferToUserProjectsJwtId,
      );
      this.newOffersToUserProjects = offersToUserProjectsData.data;
      this.newOffersToUserProjectsHitLimit = offersToUserProjectsData.hitLimit;

      // Load starred project changes if user has starred projects
      if (this.starredPlanHandleIds.length > 0) {
        try {
          const starredProjectChangesData = await getStarredProjectsWithChanges(
            this.axios,
            this.apiServer,
            this.activeDid,
            this.starredPlanHandleIds,
            this.lastAckedStarredPlanChangesJwtId,
          );
          this.newStarredProjectChanges = starredProjectChangesData.data;
          this.newStarredProjectChangesHitLimit =
            starredProjectChangesData.hitLimit;

          // Analyze differences between current plans and previous claims
          this.analyzePlanDifferences(this.newStarredProjectChanges);
        } catch (error) {
          logger.warn("Failed to load starred project changes:", error);
          this.newStarredProjectChanges = [];
          this.newStarredProjectChangesHitLimit = false;
        }
      }

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (err: any) {
      logger.error("Error retrieving settings & contacts:", err);
      this.notify.error(
        err.message || "There was an error retrieving your activity.",
        TIMEOUTS.LONG,
      );
    }
  }

  async expandOffersToUserAndMarkRead() {
    this.showOffersDetails = !this.showOffersDetails;
    if (this.showOffersDetails && this.newOffersToUser.length > 0) {
      await this.$saveUserSettings(this.activeDid, {
        lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
      });
      // note that we don't update this.lastAckedOfferToUserJwtId in case they
      // later choose the last one to keep the offers as new
      this.notify.info(
        "The offers are marked as read. Click in the list to keep them unread.",
        TIMEOUTS.LONG,
      );
    }
  }

  async markOffersAsReadStartingWith(jwtId: string) {
    const index = this.newOffersToUser.findIndex(
      (offer) => offer.jwtId === jwtId,
    );
    if (index !== -1 && index < this.newOffersToUser.length - 1) {
      // Set to the next offer's jwtId
      await this.$saveUserSettings(this.activeDid, {
        lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
      });
    } else {
      // it's the last entry (or not found), so just keep it the same
      await this.$saveUserSettings(this.activeDid, {
        lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
      });
    }
    this.notify.info(
      "All offers above that line are marked as unread.",
      TIMEOUTS.STANDARD,
    );
  }

  async expandOffersToUserProjectsAndMarkRead() {
    this.showOffersToUserProjectsDetails =
      !this.showOffersToUserProjectsDetails;
    if (
      this.showOffersToUserProjectsDetails &&
      this.newOffersToUserProjects.length > 0
    ) {
      await this.$saveUserSettings(this.activeDid, {
        lastAckedOfferToUserProjectsJwtId:
          this.newOffersToUserProjects[0].jwtId,
      });
      // note that we don't update this.lastAckedOfferToUserProjectsJwtId in case
      // they later choose the last one to keep the offers as new
      this.notify.info(
        "The offers are now marked read. Click in the list to keep them unread.",
        TIMEOUTS.LONG,
      );
    }
  }

  async markOffersToUserProjectsAsReadStartingWith(jwtId: string) {
    const index = this.newOffersToUserProjects.findIndex(
      (offer) => offer.jwtId === jwtId,
    );
    if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
      // Set to the next offer's jwtId
      await this.$saveUserSettings(this.activeDid, {
        lastAckedOfferToUserProjectsJwtId:
          this.newOffersToUserProjects[index + 1].jwtId,
      });
    } else {
      // it's the last entry (or not found), so just keep it the same
      await this.$saveUserSettings(this.activeDid, {
        lastAckedOfferToUserProjectsJwtId:
          this.lastAckedOfferToUserProjectsJwtId,
      });
    }
    this.notify.info(
      "All offers above that line are marked as unread.",
      TIMEOUTS.STANDARD,
    );
  }

  async handleSeeAllOffersToUser() {
    this.$router.push("/recent-offers-to-user");
  }

  async handleSeeAllOffersToUserProjects() {
    this.$router.push("/recent-offers-to-user-projects");
  }

  async expandStarredProjectChangesAndMarkRead() {
    this.showStarredProjectChangesDetails =
      !this.showStarredProjectChangesDetails;
    if (
      this.showStarredProjectChangesDetails &&
      this.newStarredProjectChanges.length > 0
    ) {
      await this.$saveUserSettings(this.activeDid, {
        lastAckedStarredPlanChangesJwtId:
          this.newStarredProjectChanges[0].plan.jwtId,
      });
      this.notify.info(
        "The starred project changes are now marked read. Click in the list to keep them unread.",
        TIMEOUTS.LONG,
      );
    }
  }

  async markStarredProjectChangesAsReadStartingWith(jwtId: string) {
    const index = this.newStarredProjectChanges.findIndex(
      (change) => change.plan.jwtId === jwtId,
    );
    if (index !== -1 && index < this.newStarredProjectChanges.length - 1) {
      // Set to the next change's jwtId
      await this.$saveUserSettings(this.activeDid, {
        lastAckedStarredPlanChangesJwtId:
          this.newStarredProjectChanges[index + 1].plan.jwtId,
      });
    } else {
      // it's the last entry (or not found), so just keep it the same
      await this.$saveUserSettings(this.activeDid, {
        lastAckedStarredPlanChangesJwtId: this.lastAckedStarredPlanChangesJwtId,
      });
    }
    this.notify.info(
      "All starred project changes above that line are marked as unread.",
      TIMEOUTS.STANDARD,
    );
  }

  /**
   * Analyzes differences between current plans and their previous claims
   *
   * Walks through a list of PlanSummaryAndPreviousClaim items and stores the
   * differences between the previous claim and the current plan. This method
   * extracts the claim from the wrappedClaimBefore object and compares relevant
   * fields with the current plan.
   *
   * @param planChanges Array of PlanSummaryAndPreviousClaim objects to analyze
   */
  analyzePlanDifferences(planChanges: Array<PlanSummaryAndPreviousClaim>) {
    this.planDifferences = {};

    for (const planChange of planChanges) {
      const currentPlan: PlanSummaryRecord = planChange.plan;
      const wrappedClaim: GenericCredWrapper<PlanActionClaim> | undefined =
        planChange.wrappedClaimBefore;

      // Extract the actual claim from the wrapped claim
      let previousClaim: PlanActionClaim | undefined;

      const embeddedClaim: PlanActionClaim | undefined = wrappedClaim?.claim;
      if (
        embeddedClaim &&
        typeof embeddedClaim === "object" &&
        "credentialSubject" in embeddedClaim
      ) {
        // It's a Verifiable Credential
        previousClaim =
          (embeddedClaim.credentialSubject as PlanActionClaim) || embeddedClaim;
      } else {
        // It's a direct claim
        previousClaim = embeddedClaim;
      }

      if (!previousClaim) {
        // Can happen when a project is starred after the stored last-seen-change-jwt ID
        // so we'll just leave the message saying there are no important differences.
        continue;
      }

      const differences: Record<string, { old: unknown; new: unknown }> = {};

      // Compare name
      const normalizedOldName = this.normalizeValueForComparison(
        previousClaim.name,
      );
      const normalizedNewName = this.normalizeValueForComparison(
        currentPlan.name,
      );
      if (!R.equals(normalizedOldName, normalizedNewName)) {
        differences.name = {
          old: previousClaim.name,
          new: currentPlan.name,
        };
      }

      // Compare description
      const normalizedOldDescription = this.normalizeValueForComparison(
        previousClaim.description,
      );
      const normalizedNewDescription = this.normalizeValueForComparison(
        currentPlan.description,
      );
      if (!R.equals(normalizedOldDescription, normalizedNewDescription)) {
        differences.description = {
          old: previousClaim.description,
          new: currentPlan.description,
        };
      }

      // Compare location (combine latitude and longitude into one row)
      const oldLat = this.normalizeValueForComparison(
        previousClaim.location?.geo?.latitude,
      );
      const oldLon = this.normalizeValueForComparison(
        previousClaim.location?.geo?.longitude,
      );
      const newLat = this.normalizeValueForComparison(currentPlan.locLat);
      const newLon = this.normalizeValueForComparison(currentPlan.locLon);

      if (!R.equals(oldLat, newLat) || !R.equals(oldLon, newLon)) {
        differences.location = {
          old: this.formatLocationValue(oldLat, oldLon, true),
          new: this.formatLocationValue(newLat, newLon, false),
        };
      }

      // Compare agent (issuer)
      const oldAgent = didInfoOrNobody(
        previousClaim.agent?.identifier,
        this.activeDid,
        this.allMyDids,
        this.allContacts,
      );
      const newAgent = didInfoOrNobody(
        currentPlan.agentDid,
        this.activeDid,
        this.allMyDids,
        this.allContacts,
      );
      const normalizedOldAgent = this.normalizeValueForComparison(oldAgent);
      const normalizedNewAgent = this.normalizeValueForComparison(newAgent);
      if (!R.equals(normalizedOldAgent, normalizedNewAgent)) {
        differences.agent = {
          old: oldAgent,
          new: newAgent,
        };
      }

      // Compare start time
      const oldStartTime = previousClaim.startTime;
      const newStartTime = currentPlan.startTime;
      const normalizedOldStartTime =
        this.normalizeDateForComparison(oldStartTime);
      const normalizedNewStartTime =
        this.normalizeDateForComparison(newStartTime);
      if (!R.equals(normalizedOldStartTime, normalizedNewStartTime)) {
        differences.startTime = {
          old: oldStartTime,
          new: newStartTime,
        };
      }

      // Compare end time
      const oldEndTime = previousClaim.endTime;
      const newEndTime = currentPlan.endTime;
      const normalizedOldEndTime = this.normalizeDateForComparison(oldEndTime);
      const normalizedNewEndTime = this.normalizeDateForComparison(newEndTime);
      if (!R.equals(normalizedOldEndTime, normalizedNewEndTime)) {
        differences.endTime = {
          old: oldEndTime,
          new: newEndTime,
        };
      }

      // Compare image
      const oldImage = previousClaim.image;
      const newImage = currentPlan.image;
      const normalizedOldImage = this.normalizeValueForComparison(oldImage);
      const normalizedNewImage = this.normalizeValueForComparison(newImage);
      if (!R.equals(normalizedOldImage, normalizedNewImage)) {
        differences.image = {
          old: oldImage,
          new: newImage,
        };
      }

      // Compare url
      const oldUrl = previousClaim.url;
      const newUrl = currentPlan.url;
      const normalizedOldUrl = this.normalizeValueForComparison(oldUrl);
      const normalizedNewUrl = this.normalizeValueForComparison(newUrl);
      if (!R.equals(normalizedOldUrl, normalizedNewUrl)) {
        differences.url = {
          old: oldUrl,
          new: newUrl,
        };
      }

      // Store differences if any were found
      if (!R.isEmpty(differences)) {
        this.planDifferences[currentPlan.handleId] = differences;
        logger.debug(
          "[NewActivityView] Plan differences found for",
          currentPlan.handleId,
          differences,
        );
      }
    }

    logger.debug(
      "[NewActivityView] Analyzed",
      planChanges.length,
      "plan changes, found differences in",
      Object.keys(this.planDifferences).length,
      "plans",
    );
  }

  /**
   * Normalizes values for comparison - treats null, undefined, and empty string as equivalent
   *
   * @param value The value to normalize
   * @returns The normalized value (null for null/undefined/empty, otherwise the original value)
   */
  normalizeValueForComparison<T>(value: T | null | undefined): T | null {
    if (value === null || value === undefined || value === "") {
      return null;
    }
    return value;
  }

  /**
   * Normalizes date values for comparison by converting strings to Date objects
   * Returns null for null/undefined/empty values, Date objects for valid date strings
   */
  normalizeDateForComparison(value: unknown): Date | null {
    if (value === null || value === undefined || value === "") {
      return null;
    }
    if (typeof value === "string") {
      const date = new Date(value);
      // Check if the date is valid
      return isNaN(date.getTime()) ? null : date;
    }
    if (value instanceof Date) {
      return isNaN(value.getTime()) ? null : value;
    }
    return null;
  }

  /**
   * Gets the differences for a specific plan by handle ID
   *
   * @param handleId The handle ID of the plan to get differences for
   * @returns The differences object or null if no differences found
   */
  getPlanDifferences(
    handleId: string,
  ): Record<string, { old: unknown; new: unknown }> | null {
    return this.planDifferences[handleId] || null;
  }

  /**
   * Formats a field value for display in the UI
   *
   * @param value The value to format
   * @returns A human-readable string representation
   */
  formatFieldValue(value: unknown): string {
    if (value === null || value === undefined) {
      return "Not set";
    }
    if (typeof value === "string") {
      const stringValue = value || "Empty";

      // Check if it's a date/time string
      if (this.isDateTimeString(stringValue)) {
        return this.formatDateTime(stringValue);
      }

      // Check if it's a URL
      if (this.isUrl(stringValue)) {
        return stringValue; // Keep URLs as-is for now
      }

      return stringValue;
    }
    if (typeof value === "number") {
      return value.toString();
    }
    if (typeof value === "boolean") {
      return value ? "Yes" : "No";
    }
    // For complex objects, stringify
    const stringified = JSON.stringify(value);
    return stringified;
  }

  /**
   * Checks if a string appears to be a date/time string
   */
  isDateTimeString(value: string): boolean {
    if (!value) return false;
    // Check for ISO 8601 format or other common date formats
    const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?(\.\d{3})?Z?$/;
    return dateRegex.test(value) || !isNaN(Date.parse(value));
  }

  /**
   * Checks if a string is a URL
   */
  isUrl(value: string): boolean {
    if (!value) return false;
    try {
      new URL(value);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Formats a date/time string for display
   */
  formatDateTime(value: string): string {
    try {
      const date = new Date(value);
      return date.toLocaleString();
    } catch {
      return value; // Return original if parsing fails
    }
  }

  /**
   * Gets a human-readable field name for display
   *
   * @param fieldName The internal field name
   * @returns A formatted field name for display
   */
  getDisplayFieldName(fieldName: string): string {
    const fieldNameMap: Record<string, string> = {
      name: "Name",
      description: "Description",
      location: "Location",
      agent: "Agent",
      startTime: "Start Time",
      endTime: "End Time",
      image: "Image",
      url: "URL",
    };
    return fieldNameMap[fieldName] || fieldName;
  }

  /**
   * Formats location values for display
   *
   * @param latitude The latitude value
   * @param longitude The longitude value
   * @param isOldValue Whether this is the old value (true) or new value (false)
   * @returns A formatted location string
   */
  formatLocationValue(
    latitude: number | undefined | null,
    longitude: number | undefined | null,
    isOldValue: boolean = false,
  ): string {
    if (latitude == null && longitude == null) {
      return "Not set";
    }
    // If there's any location data, show generic labels instead of coordinates
    if (isOldValue) {
      return "A Location";
    } else {
      return "New Location";
    }
  }

  /**
   * Computes the line-by-line diff between two description strings
   *
   * @param oldValue The old description value
   * @param newValue The new description value
   * @returns An array of Change objects from the diff library
   */
  getDescriptionDiff(oldValue: unknown, newValue: unknown): Change[] {
    const oldStr = this.formatFieldValue(oldValue);
    const newStr = this.formatFieldValue(newValue);
    return diffLines(oldStr, newStr);
  }

  /**
   * Gets the background color style for a diff change
   *
   * @param change A Change object from the diff library
   * @returns A CSS style object with background color
   */
  getDiffStyle(change: Change): Record<string, string> {
    if (change.added) {
      return { backgroundColor: "#d4edda" }; // Light green
    }
    if (change.removed) {
      return { backgroundColor: "#f8d7da" }; // Light red
    }
    return {};
  }
}
</script>
