<template>
  <QuickNav selected="Projects"></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">
        Edit Project Idea
      </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>

    <!-- Project Details -->
    <!-- Image - (see design model) Empty -->

    <div>
      {{ errorMessage }}
    </div>

    <input
      v-model="fullClaim.name"
      type="text"
      placeholder="Idea Name"
      class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
    />

    <div class="flex justify-center mt-4">
      <span v-if="hasImage" class="flex justify-between">
        <a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
          <img :src="imageUrl" class="h-24 rounded-xl" />
        </a>
        <font-awesome
          icon="trash-can"
          class="text-red-500 fa-fw ml-8 mt-10"
          @click="confirmDeleteImage"
        />
      </span>
      <span v-else>
        <font-awesome
          icon="camera"
          :class="cameraIconClasses"
          @click="openImageDialog"
        />
      </span>
    </div>
    <ImageMethodDialog ref="imageDialog" default-camera-mode="environment" />

    <input
      v-model="agentDid"
      type="text"
      placeholder="Other Authorized Representative"
      class="mt-4 block w-full rounded border border-slate-400 px-3 py-2"
    />
    <div class="mb-4">
      <p v-if="shouldShowOwnershipWarning">
        <span class="text-red-500">Beware!</span>
        If you save this, the original project owner will no longer be able to
        edit it.
        <button class="text-blue-500" @click="agentDid = projectIssuerDid">
          Click here to make the original owner an authorized representative.
        </button>
      </p>
    </div>

    <textarea
      v-model="fullClaim.description"
      placeholder="Description"
      class="block w-full rounded border border-slate-400 px-3 py-2"
      rows="5"
      maxlength="5000"
    ></textarea>
    <div class="text-xs text-slate-500 italic">
      If you want to be contacted, be sure to include your contact information
      -- just remember that this information is public and saved in a public
      history.
    </div>
    <div class="text-xs text-slate-500 italic">
      {{ descriptionCharacterCount }}
    </div>

    <input
      v-model="fullClaim.url"
      placeholder="Website"
      autocapitalize="none"
      class="block w-full rounded border border-slate-400 mt-4 px-3 py-2"
    />

    <div>
      <div class="flex items-center mt-4">
        <span class="mr-2">Starts At</span>
        <input
          v-model="startDateInput"
          placeholder="Start Date"
          type="date"
          class="rounded border border-slate-400 px-3 py-2"
        />
        <input
          v-model="startTimeInput"
          :disabled="!startDateInput"
          placeholder="Start Time"
          type="time"
          class="rounded border border-slate-400 ml-2 px-3 py-2"
        />
      </div>

      <div class="flex w-full justify-end items-center">
        <span class="w-full flex justify-end items-center">
          {{ timezoneDisplay }}
        </span>
      </div>

      <div class="flex items-center">
        <div class="mr-2">
          <span>Ends at</span>
        </div>
        <input
          v-model="endDateInput"
          placeholder="End Date"
          type="date"
          class="ml-2 rounded border border-slate-400 px-3 py-2"
        />
        <input
          v-model="endTimeInput"
          :disabled="!endDateInput"
          placeholder="End Time"
          type="time"
          class="rounded border border-slate-400 ml-2 px-3 py-2"
        />
      </div>
    </div>

    <div
      class="flex items-center mt-4"
      @click="includeLocation = !includeLocation"
    >
      <input v-model="includeLocation" type="checkbox" class="mr-2" />
      <label for="includeLocation">Include Location</label>
    </div>
    <div v-if="includeLocation" class="mb-4 aspect-video">
      <p class="text-sm mb-2 text-slate-500">
        For your security, choose a location nearby but not exactly at the
        place.
      </p>

      <l-map
        ref="map"
        v-model:zoom="zoom"
        :center="[0, 0]"
        class="!z-40 rounded-md"
        @click="onMapClick"
      >
        <l-tile-layer
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
          layer-type="base"
          name="OpenStreetMap"
        />
        <l-marker
          v-if="shouldShowMapMarker"
          :lat-lng="[latitude, longitude]"
          @click="confirmEraseLatLong()"
        />
      </l-map>
    </div>

    <div v-if="shouldShowPartnerOptions" class="items-center mb-4">
      <div class="flex" @click="sendToTrustroots = !sendToTrustroots">
        <input v-model="sendToTrustroots" type="checkbox" class="mr-2" />
        <label>Send to Trustroots</label>
        <font-awesome
          icon="circle-info"
          class="text-blue-500 ml-2 cursor-pointer"
          @click.stop="showNostrPartnerInfo"
        />
      </div>
    </div>

    <div class="mt-8">
      <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
        <button
          :disabled="isHiddenSave"
          :class="saveButtonClasses"
          @click="onSaveProjectClick()"
        >
          <!-- SHOW if in idle state -->
          <span :class="{ hidden: !shouldShowSaveText }">Save Project</span>

          <!-- SHOW if in saving state; DISABLE button while in saving state -->
          <span :class="{ hidden: !shouldShowSpinner }">
            <!-- icon no worky? -->
            <i class="fa-solid fa-spinner fa-spin-pulse"></i>
            Saving...</span
          >
        </button>
        <button
          type="button"
          :class="cancelButtonClasses"
          @click="onCancelClick()"
        >
          Cancel
        </button>
      </div>
    </div>
  </section>
</template>

<script lang="ts">
import "leaflet/dist/leaflet.css";
import { AxiosError, AxiosRequestHeaders } from "axios";
import { DateTime } from "luxon";
import { finalizeEvent } from "@nostr/tools";
import {
  accountFromExtendedKey,
  extendedKeysFromSeedWords,
} from "@nostr/tools/nip06";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { LeafletMouseEvent } from "leaflet";

import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import {
  DEFAULT_IMAGE_API_SERVER,
  DEFAULT_PARTNER_API_SERVER,
  NotificationIface,
} from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers } from "../utils/notify";
import {
  NOTIFY_PROJECT_ACCOUNT_LOADING_ERROR,
  NOTIFY_PROJECT_RETRIEVAL_ERROR,
  NOTIFY_PROJECT_DELETE_IMAGE_CONFIRM,
  NOTIFY_PROJECT_DELETE_IMAGE_ERROR,
  NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR,
  NOTIFY_PROJECT_INVALID_LOCATION,
  NOTIFY_PROJECT_INVALID_START_DATE,
  NOTIFY_PROJECT_INVALID_END_DATE,
  NOTIFY_PROJECT_SAVE_SUCCESS,
  NOTIFY_PROJECT_PARTNER_LOCATION_WARNING,
  NOTIFY_PROJECT_PARTNER_SEND_GENERAL_ERROR,
  NOTIFY_PROJECT_DELETE_LOCATION_CONFIRM,
  NOTIFY_PROJECT_NOSTR_PARTNER_INFO,
  createProjectPartnerSendSuccessMessage,
  createProjectPartnerSendErrorMessage,
  PROJECT_TIMEOUT_VERY_LONG,
} from "../constants/notifications";
import { PlanActionClaim } from "../interfaces/claims";
import {
  createEndorserJwtVcFromClaim,
  getHeaders,
} from "../libs/endorserServer";
import {
  retrieveAccountCount,
  retrieveFullyDecryptedAccount,
} from "../libs/util";

import {
  EventTemplate,
  UnsignedEvent,
  VerifiedEvent,
  serializeEvent,
} from "@nostr/tools";

import { logger } from "../utils/logger";

/**
 * @fileoverview NewEditProjectView - Project Creation and Editing Interface
 *
 * This component provides a comprehensive interface for creating and editing project ideas
 * within the TimeSafari ecosystem. It supports rich project data including images, locations,
 * dates, and integration with external partner services.
 *
 * Key Features:
 * - Project CRUD operations (create, read, update, delete)
 * - Rich form fields for comprehensive project information
 * - Image upload and management with deletion capabilities
 * - Interactive map integration for location selection
 * - Partner service integration (Trustroots, TripHopping)
 * - Date/time validation and timezone handling
 * - Cryptographic signing for partner authentication
 * - Comprehensive error handling and user feedback
 *
 * Enhanced Triple Migration Pattern Status:
 * ✅ Phase 1: Database Migration - PlatformServiceMixin integration
 * ⏳ Phase 2: SQL Abstraction - No raw SQL queries to migrate
 * ⏳ Phase 3: Notification Migration - 16 notification calls to standardize
 * ⏳ Phase 4: Template Streamlining - Multiple candidates for computed properties
 *
 * External Dependencies:
 * - Leaflet Maps: Geographic location selection
 * - Axios: API communication for project and image operations
 * - Luxon: Date/time manipulation and timezone handling
 * - Nostr Tools: Cryptographic signing for partner services
 * - Image API: Upload and deletion of project images
 *
 * Security: Component handles sensitive cryptographic operations for partner
 * integration and requires proper authentication for all API operations.
 *
 * @component NewEditProjectView
 * @requires PlatformServiceMixin - Database operations and account management
 * @requires ImageMethodDialog - Image upload and management
 * @requires QuickNav - Navigation component
 * @requires Leaflet - Interactive map functionality
 * @author TimeSafari Development Team
 * @since 2024-01-01
 * @version 1.0.0
 * @migrated 2025-07-09 (Enhanced Triple Migration Pattern - Phase 1)
 */

@Component({
  components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
  mixins: [PlatformServiceMixin],
})
export default class NewEditProjectView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  $route!: RouteLocationNormalizedLoaded;
  $router!: Router;

  // Notification helpers
  private notify!: ReturnType<typeof createNotifyHelpers>;

  /**
   * Display error notification to user
   * Provides consistent error messaging with 5-second timeout
   * @param message - Error message to display
   */
  errNote(message: string) {
    this.notify.error(message);
  }

  // Component state properties
  activeDid = "";
  agentDid = "";
  apiServer = "";
  endDateInput?: string;
  endTimeInput?: string;
  errorMessage = "";
  fullClaim: PlanActionClaim = {
    "@context": "https://schema.org",
    "@type": "PlanAction",
    name: "",
    description: "",
  }; // this default is only to avoid errors before plan is loaded
  imageUrl = "";
  includeLocation = false;
  isHiddenSave = false;
  isHiddenSpinner = true;
  lastClaimJwtId = "";
  latitude = 0;
  longitude = 0;
  numAccounts = 0;
  projectId = "";
  projectIssuerDid = "";
  sendToTrustroots = false;
  sendToTripHopping = false;
  showGeneralAdvanced = false;
  startDateInput?: string;
  startTimeInput?: string;
  zoneName = DateTime.local().zoneName;
  zoom = 2;

  /**
   * Component lifecycle hook - Initialize project editing interface
   * Loads user account information and project data if editing existing project
   * Handles account validation and project loading with comprehensive error handling
   */
  async mounted() {
    // Initialize notification helpers
    this.notify = createNotifyHelpers(this.$notify);

    this.numAccounts = await retrieveAccountCount();

    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.showGeneralAdvanced = !!settings.showGeneralAdvanced;

    this.projectId = (this.$route.query["projectId"] as string) || "";

    if (this.projectId) {
      if (this.numAccounts === 0) {
        this.notify.error(NOTIFY_PROJECT_ACCOUNT_LOADING_ERROR.message);
      } else {
        this.loadProject(this.activeDid);
      }
    }
  }

  /**
   * Load existing project data for editing
   * Retrieves project information from the API and populates form fields
   * @param userDid - User's decentralized identifier
   */
  async loadProject(userDid: string) {
    const url =
      this.apiServer +
      "/api/claim/byHandle/" +
      encodeURIComponent(this.projectId);
    const headers = await getHeaders(userDid);

    try {
      const resp = await this.axios.get(url, { headers });
      if (resp.status === 200) {
        this.projectIssuerDid = resp.data.issuer;
        this.fullClaim = resp.data.claim;
        this.imageUrl = resp.data.claim.image || "";
        this.lastClaimJwtId = resp.data.id;
        if (this.fullClaim?.location) {
          this.includeLocation = true;
          this.latitude = this.fullClaim.location.geo.latitude;
          this.longitude = this.fullClaim.location.geo.longitude;
        }
        if (this.fullClaim?.agent?.identifier) {
          this.agentDid = this.fullClaim.agent.identifier;
        }
        if (this.fullClaim.startTime) {
          const localDateTime = DateTime.fromISO(
            this.fullClaim.startTime as string,
          ).toLocal();
          this.startDateInput = localDateTime.toFormat("yyyy-MM-dd");
          this.startTimeInput = localDateTime.toFormat("HH:mm");
        }
        if (this.fullClaim.endTime) {
          const localDateTime = DateTime.fromISO(
            this.fullClaim.endTime as string,
          ).toLocal();
          this.endDateInput = localDateTime.toFormat("yyyy-MM-dd");
          this.endTimeInput = localDateTime.toFormat("HH:mm");
        }
      }
    } catch (error) {
      logger.error("Got error retrieving that project", error);
      this.notify.error(NOTIFY_PROJECT_RETRIEVAL_ERROR.message);
    }
  }

  /**
   * Open image upload dialog
   * Integrates with ImageMethodDialog component for image selection and upload
   */
  openImageDialog() {
    (this.$refs.imageDialog as ImageMethodDialog).open((imgUrl) => {
      this.imageUrl = imgUrl;
    }, "PlanAction");
  }

  /**
   * Confirm image deletion with user
   * Shows confirmation dialog before proceeding with image deletion
   */
  confirmDeleteImage() {
    this.notify.confirm(
      NOTIFY_PROJECT_DELETE_IMAGE_CONFIRM.message,
      async () => {
        await this.deleteImage();
      },
    );
  }

  /**
   * Delete project image from server
   * Handles API call to delete image and updates component state
   * Includes comprehensive error handling for various failure scenarios
   */
  async deleteImage() {
    if (!this.imageUrl) {
      return;
    }
    try {
      const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders;
      if (
        window.location.hostname === "localhost" &&
        !DEFAULT_IMAGE_API_SERVER.includes("localhost")
      ) {
        logger.log(
          "Using shared image API server, so only users on that server can play with images.",
        );
      }

      const response = await this.axios.delete(
        DEFAULT_IMAGE_API_SERVER +
          "/image/" +
          encodeURIComponent(this.imageUrl),
        { headers },
      );
      if (response.status === 204) {
        // don't bother with a notification
        // (either they'll simply continue or they're canceling and going back)
      } else {
        logger.error("Problem deleting image:", response);
        this.notify.error(NOTIFY_PROJECT_DELETE_IMAGE_ERROR.message);
        return;
      }

      this.imageUrl = "";
    } catch (error) {
      logger.error("Error deleting image:", error);
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      if ((error as any).response.status === 404) {
        logger.log("The image was already deleted:", error);

        this.imageUrl = "";

        // it already doesn't exist so we won't say anything to the user
      } else {
        this.notify.error(NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR.message);
      }
    }
  }

  /**
   * Save project data to server
   * Handles project creation and editing with comprehensive validation
   * Includes partner service integration and error handling
   */
  private async saveProject() {
    // Make a claim
    const vcClaim: PlanActionClaim = this.fullClaim;
    if (this.projectId) {
      vcClaim.lastClaimId = this.lastClaimJwtId;
    }
    if (this.agentDid) {
      vcClaim.agent = {
        identifier: this.agentDid,
      };
    } else {
      delete vcClaim.agent;
    }
    if (this.imageUrl) {
      vcClaim.image = this.imageUrl;
    } else {
      delete vcClaim.image;
    }
    if (this.includeLocation) {
      if (!this.latitude || !this.longitude) {
        this.notify.error(NOTIFY_PROJECT_INVALID_LOCATION.message);
        delete vcClaim.location;
      } else {
        vcClaim.location = {
          geo: {
            "@type": "GeoCoordinates",
            latitude: this.latitude,
            longitude: this.longitude,
          },
        };
      }
    } else {
      delete vcClaim.location;
    }
    if (this.startDateInput) {
      try {
        const startTimeFull = this.startTimeInput || "00:00:00";
        const fullTimeString = this.startDateInput + " " + startTimeFull;
        // throw an error on an invalid date or time string
        vcClaim.startTime = new Date(fullTimeString).toISOString(); // ensure timezone is part of it
      } catch {
        // it's not a valid date so erase it and tell the user
        delete vcClaim.startTime;
        this.notify.error(NOTIFY_PROJECT_INVALID_START_DATE.message);
      }
    } else {
      delete vcClaim.startTime;
    }
    if (this.endDateInput) {
      try {
        const endTimeFull = this.endTimeInput || "23:59:59";
        const fullTimeString = this.endDateInput + " " + endTimeFull;
        // throw an error on an invalid date or time string
        vcClaim.endTime = new Date(fullTimeString).toISOString(); // ensure timezone is part of it
      } catch {
        // it's not a valid date so erase it and tell the user
        delete vcClaim.endTime;
        this.notify.error(NOTIFY_PROJECT_INVALID_END_DATE.message);
      }
    } else {
      delete vcClaim.endTime;
    }
    const vcJwt = await createEndorserJwtVcFromClaim(this.activeDid, vcClaim);

    // Make the xhr request payload

    const payload = JSON.stringify({ jwtEncoded: vcJwt });
    const url = this.apiServer + "/api/v2/claim";
    const headers = await getHeaders(this.activeDid);

    try {
      const resp = await this.axios.post(url, payload, { headers });
      if (resp.data?.success?.handleId) {
        this.notify.success(NOTIFY_PROJECT_SAVE_SUCCESS.message);

        this.errorMessage = "";

        const projectPath = encodeURIComponent(resp.data.success.handleId);

        if (this.sendToTrustroots || this.sendToTripHopping) {
          if (this.latitude && this.longitude) {
            let payloadAndKey; // sign something to prove ownership of pubkey
            if (this.sendToTrustroots) {
              payloadAndKey = await this.signSomePayload();
              // not going to await... the save was successful, so we'll continue to the next page
              this.sendToNostrPartner(
                "NOSTR-EVENT-TRUSTROOTS",
                "Trustroots",
                resp.data.success.claimId,
                payloadAndKey.signedEvent,
                payloadAndKey.publicExtendedKey,
              );
            }
            if (this.sendToTripHopping) {
              if (!payloadAndKey) {
                payloadAndKey = await this.signSomePayload();
              }
              // not going to await... the save was successful, so we'll continue to the next page
              this.sendToNostrPartner(
                "NOSTR-EVENT-TRIPHOPPING",
                "TripHopping",
                resp.data.success.claimId,
                payloadAndKey.signedEvent,
                payloadAndKey.publicExtendedKey,
              );
            }
          } else {
            this.notify.error(NOTIFY_PROJECT_PARTNER_LOCATION_WARNING.message);
          }
        }

        this.$router.push({ path: "/project/" + projectPath });
      } else {
        logger.error("Got unexpected 'data' inside response from server", resp);
        let userMessage = JSON.stringify(resp.data);
        if (resp.data?.error?.message) {
          userMessage = resp.data.error.message;
        }
        // Now set that error for the user to see.
        this.errorMessage = userMessage;
      }
    } catch (error) {
      logger.error("Got error saving project", error);
      let userMessage = "There was an error saving the project.";
      if (error instanceof AxiosError) {
        if (error.response?.status === 400) {
          userMessage = "The project information was invalid.";
        } else if (error.response?.status === 401) {
          userMessage = "You are not authorized to perform this action.";
        } else if (error.response?.status === 403) {
          userMessage = "You are not authorized to edit this project.";
        } else if (error.response?.status === 404) {
          userMessage = "The project was not found.";
        } else if (error.response?.status === 409) {
          userMessage = "There was a conflict with the project data.";
        } else if (error.response?.status === 422) {
          userMessage = "The project data was invalid.";
        } else if (error.response?.status === 500) {
          userMessage = "There was a server error.";
        }
        if (error.response?.data?.error?.message) {
          userMessage = error.response.data.error.message;
        }
      }
      if (userMessage) {
        this.notify.error(userMessage);
      }
      // Now set that error for the user to see.
      this.errorMessage = userMessage;
    }
  }

  /**
   * Generate signed payload for partner authentication
   * Creates cryptographic signature for external partner services
   * @returns Promise containing signed event and public extended key
   */
  private async signSomePayload(): Promise<{
    signedEvent: VerifiedEvent;
    publicExtendedKey: string;
  }> {
    const account = await retrieveFullyDecryptedAccount(this.activeDid);
    // get the last number of the derivationPath
    const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
    // remove any trailing '
    const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
    const accountNum = Number(finalDerNumNoApostrophe || 0);
    const extPubPri = extendedKeysFromSeedWords(
      account?.mnemonic as string,
      "",
      accountNum,
    );
    const publicExtendedKey: string = extPubPri?.publicExtendedKey;
    const privateExtendedKey = extPubPri?.privateExtendedKey;
    const privateBytes: Uint8Array =
      accountFromExtendedKey(privateExtendedKey).privateKey ||
      (() => {
        throw new Error("Failed to derive private key");
      })();
    // No real content is necessary, we just want something signed,
    // so we might as well use nostr libs for nostr functions.
    // Besides: someday we may create real content that we can relay.
    const event: EventTemplate = {
      kind: 30402,
      tags: [[]],
      content: "",
      created_at: 0,
    };
    const signedEvent: VerifiedEvent = finalizeEvent(
      // Why does IntelliJ not see matching types?
      event as EventTemplate,
      privateBytes,
    ) as VerifiedEvent;
    return { signedEvent, publicExtendedKey };
  }

  /**
   * Send project information to external partner service
   * Integrates with Nostr-based partner services like Trustroots and TripHopping
   * @param linkCode - Service-specific link code identifier
   * @param serviceName - Human-readable service name
   * @param jwtId - JWT identifier for the project claim
   * @param signedPayload - Cryptographically signed payload
   * @param publicExtendedKey - Public key for verification
   */
  private async sendToNostrPartner(
    linkCode: string,
    serviceName: string,
    jwtId: string,
    signedPayload: VerifiedEvent,
    publicExtendedKey: string,
  ) {
    try {
      let partnerServer = DEFAULT_PARTNER_API_SERVER;
      const settings = await this.$accountSettings();
      if (settings.partnerApiServer) {
        partnerServer = settings.partnerApiServer;
      }
      const endorserPartnerUrl = partnerServer + "/api/partner/link";
      const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
      const content = this.fullClaim.name + " - see " + timeSafariUrl;
      const publicKeyHex = accountFromExtendedKey(publicExtendedKey).publicKey;
      const unsignedPayload: UnsignedEvent = {
        // why doesn't "...signedPayload" work?
        kind: signedPayload.kind,
        tags: signedPayload.tags,
        content: signedPayload.content,
        created_at: signedPayload.created_at,
        pubkey: publicKeyHex,
      };
      // Why does IntelliJ not see matching types?
      const payload = serializeEvent(unsignedPayload as UnsignedEvent);
      const partnerParams = {
        jwtId: jwtId,
        linkCode: linkCode,
        inputJson: JSON.stringify(content),
        pubKeyHex: publicKeyHex,
        pubKeyImage: payload,
        pubKeySigHex: signedPayload.sig,
      };
      const headers = await getHeaders(this.activeDid);
      const linkResp = await this.axios.post(
        endorserPartnerUrl,
        partnerParams,
        { headers },
      );
      if (linkResp.status === 201) {
        this.notify.success(
          createProjectPartnerSendSuccessMessage(serviceName),
        );
      } else {
        // axios never gets here because it throws an error, but just in case
        this.notify.error(
          createProjectPartnerSendErrorMessage(
            serviceName,
            JSON.stringify(linkResp.data),
          ),
        );
      }
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (error: any) {
      logger.error(`Error sending to ${serviceName}`, error);
      let errorMessage = NOTIFY_PROJECT_PARTNER_SEND_GENERAL_ERROR.message;
      if (error.response?.data?.error?.message) {
        errorMessage = error.response.data.error.message;
      }
      this.notify.error(errorMessage, PROJECT_TIMEOUT_VERY_LONG);
    }
  }

  /**
   * Handle save project button click
   * Manages loading state and initiates project save operation
   */
  public async onSaveProjectClick() {
    this.isHiddenSave = true;
    this.isHiddenSpinner = false;

    if (this.numAccounts === 0) {
      logger.error("Error: there is no account.");
    } else {
      this.saveProject();
    }
  }

  /**
   * Confirm location marker erasure
   * Shows confirmation dialog before clearing location data
   */
  confirmEraseLatLong() {
    this.notify.confirm(
      NOTIFY_PROJECT_DELETE_LOCATION_CONFIRM.message,
      async () => {
        this.eraseLatLong();
      },
    );
  }

  /**
   * Clear location data
   * Resets latitude, longitude, and location inclusion state
   */
  public eraseLatLong() {
    this.latitude = 0;
    this.longitude = 0;
    this.includeLocation = false;
  }

  /**
   * Handle cancel button click
   * Returns to previous view without saving changes
   */
  public onCancelClick() {
    this.$router.back();
  }

  /**
   * Show information about Nostr partner integration
   * Displays privacy information about partner service integration
   */
  public showNostrPartnerInfo() {
    this.notify.info(
      NOTIFY_PROJECT_NOSTR_PARTNER_INFO.message,
      PROJECT_TIMEOUT_VERY_LONG,
    );
  }

  /**
   * Handle map click events
   * Updates latitude and longitude based on map click location
   * @param event - Leaflet mouse event containing clicked coordinates
   */
  onMapClick(event: LeafletMouseEvent) {
    this.latitude = event.latlng.lat;
    this.longitude = event.latlng.lng;
  }

  /**
   * Computed property for character count display
   * Shows current description length and maximum character limit
   */
  get descriptionCharacterCount(): string {
    const currentLength = this.fullClaim.description?.length || 0;
    return `${currentLength}/5000 max. characters`;
  }

  /**
   * Computed property for agent DID validation warning visibility
   * Shows warning when changing ownership from original project owner
   */
  get shouldShowOwnershipWarning(): boolean {
    return (
      this.activeDid !== this.projectIssuerDid &&
      this.agentDid !== this.projectIssuerDid
    );
  }

  /**
   * Computed property for timezone display
   * Shows current timezone name with proper formatting
   */
  get timezoneDisplay(): string {
    return `${this.zoneName} time zone`;
  }

  /**
   * Computed property for map marker visibility
   * Determines when to show the location marker on the map
   */
  get shouldShowMapMarker(): boolean {
    return !!(this.latitude && this.longitude);
  }

  /**
   * Computed property for partner service options visibility
   * Shows partner options only when advanced settings are enabled and location is included
   */
  get shouldShowPartnerOptions(): boolean {
    return this.showGeneralAdvanced && this.includeLocation;
  }

  /**
   * Computed property for save button classes
   * Provides consistent styling for the save project button
   */
  get saveButtonClasses(): string {
    return "block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2";
  }

  /**
   * Computed property for cancel button classes
   * Provides consistent styling for the cancel button
   */
  get cancelButtonClasses(): string {
    return "block w-full text-center text-md 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 px-1.5 py-2 rounded-md";
  }

  /**
   * Computed property for camera icon classes
   * Provides consistent styling for the camera icon button
   */
  get cameraIconClasses(): string {
    return "bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md";
  }

  /**
   * Computed property for image display state
   * Determines whether to show image or camera icon
   */
  get hasImage(): boolean {
    return !!this.imageUrl;
  }

  /**
   * Computed property for save button text visibility
   * Controls visibility of "Save Project" text based on loading state
   */
  get shouldShowSaveText(): boolean {
    return !this.isHiddenSave;
  }

  /**
   * Computed property for spinner visibility
   * Controls visibility of saving spinner based on loading state
   */
  get shouldShowSpinner(): boolean {
    return !this.isHiddenSpinner;
  }
}
</script>
