<template>
  <QuickNav />

  <!-- CONTENT -->
  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
    <TopMessage />

    <!-- 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">
        What Is Offered
      </h1>

      <!-- Back -->
      <a
        class="order-first text-lg text-center leading-none p-1"
        @click="cancelBack()"
      >
        <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>

    <h1 class="text-xl font-bold text-center mb-4">
      <span> Offer to {{ recipientDisplayName }} </span>
    </h1>
    <textarea
      v-model="descriptionOfItem"
      class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
      placeholder="What is offered"
      data-testId="itemDescription"
    />
    <div class="flex flex-row justify-center">
      <span
        class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
        @click="changeUnitCode()"
      >
        {{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
      </span>
      <div
        class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
        @click="amountInput === '0' ? null : decrement()"
      >
        <font-awesome icon="chevron-left" />
      </div>
      <input
        v-model="amountInput"
        type="number"
        class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
        data-testId="inputOfferAmount"
      />
      <div
        class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
        @click="increment()"
      >
        <font-awesome icon="chevron-right" />
      </div>
    </div>

    <div class="flex flex-row mt-2">
      <span
        class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
      >
        Conditions
      </span>
      <textarea
        v-model="descriptionOfCondition"
        class="w-full border border-slate-400 px-3 py-2 rounded-r"
        placeholder="Prerequisites, other people to include, etc."
      />
    </div>

    <div class="flex flex-row mt-2">
      <span
        class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
      >
        {{ validThroughDateInput ? "" : "No" }}&nbsp;Expiration
      </span>
      <input
        v-model="validThroughDateInput"
        type="date"
        class="w-full rounded border border-slate-400 px-3 py-2 rounded-r"
      />
    </div>

    <div class="h-7 mt-4 flex">
      <input
        v-if="projectId && !offeredToRecipient"
        v-model="offeredToProject"
        type="checkbox"
        class="h-6 w-6 mr-2"
      />
      <font-awesome
        v-else
        icon="square"
        class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
        @click="notifyUserOfProject()"
      />
      <label class="text-sm mt-1">
        {{ projectAssignmentLabel }}
      </label>
    </div>

    <div class="h-7 mt-4 flex">
      <input
        v-if="recipientDid && !offeredToProject"
        v-model="offeredToRecipient"
        type="checkbox"
        class="h-6 w-6 mr-2"
      />
      <font-awesome
        v-else
        icon="square"
        class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
        @click="notifyUserOfRecipient()"
      />
      <label class="text-sm mt-1">
        {{ recipientAssignmentLabel }}
      </label>
    </div>

    <div v-if="showGeneralAdvanced" class="mt-4 flex">
      <router-link
        :to="{
          name: 'claim-add-raw',
          query: {
            claim: constructOfferParam(),
          },
        }"
        class="text-blue-500"
      >
        Edit & Submit Raw
      </router-link>
    </div>

    <p class="text-center mb-2 mt-6 italic">
      Sign & Send to publish to the world
      <font-awesome
        icon="circle-info"
        class="pl-2 text-blue-500 cursor-pointer"
        @click="explainData()"
      />
    </p>
    <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
      <button
        class="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"
        @click="confirm"
      >
        Sign &amp; Send
      </button>
      <button
        class="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"
        @click="cancel"
      >
        Cancel
      </button>
    </div>
  </section>
</template>

<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";

import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { GenericCredWrapper, OfferClaim } from "../interfaces";
import {
  createAndSubmitOffer,
  didInfo,
  editAndSubmitOffer,
  getPlanFromCache,
  hydrateOffer,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import {
  NOTIFY_OFFER_ERROR_LOADING,
  NOTIFY_OFFER_ERROR_PREVIOUS_RECORD,
  NOTIFY_OFFER_ERROR_NO_IDENTIFIER,
  NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT,
  NOTIFY_OFFER_ERROR_NO_DESCRIPTION,
  NOTIFY_OFFER_PROCESSING,
  NOTIFY_OFFER_ERROR_PROJECT_ASSIGNMENT,
  NOTIFY_OFFER_ERROR_PROJECT_RECIPIENT_CONFLICT,
  NOTIFY_OFFER_ERROR_RECIPIENT_ASSIGNMENT,
  NOTIFY_OFFER_ERROR_RECIPIENT_PROJECT_CONFLICT,
  NOTIFY_OFFER_ERROR_CREATION,
  NOTIFY_OFFER_SUCCESS_RECORDED,
  NOTIFY_OFFER_ERROR_RECORDATION,
  NOTIFY_OFFER_PRIVACY_INFO,
} from "@/constants/notifications";

/**
 * Offer Details View Component
 * @author Matthew Raymer
 *
 * This component handles the creation and editing of offers within the platform.
 * It supports both new offers and editing existing ones, with validation and
 * submission handling.
 *
 * Features:
 * - Offer amount and unit selection
 * - Item description
 * - Conditional requirements
 * - Expiration date setting
 * - Project or recipient targeting
 * - Raw claim editing option
 *
 * Data Flow:
 * 1. Component loads with optional previous offer data
 * 2. Retrieves account settings and contact information
 * 3. Populates form with existing or default values
 * 4. Validates and submits offer to server
 * 5. Redirects on success or shows error
 *
 * Security Features:
 * - DID validation
 * - JWT handling for edits
 * - Server-side validation
 * - Privacy controls for data sharing
 *
 * @see GiftedDialog for related gift creation
 * @see ClaimAddRawView for raw claim editing
 */
@Component({
  components: {
    QuickNav,
    TopMessage,
  },
  mixins: [PlatformServiceMixin],
})
export default class OfferDetailsView extends Vue {
  /** Notification function injected by Vue */
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  /** Current route instance */
  $route!: RouteLocationNormalizedLoaded;
  /** Router instance for navigation */
  $router!: Router;
  /** Notification helper methods */
  notify!: ReturnType<typeof createNotifyHelpers>;

  /** Currently active DID */
  activeDid = "";
  /** API server endpoint */
  apiServer = "";
  /** Offer amount input field */
  amountInput = "0";
  /** Conditions for the offer */
  descriptionOfCondition = "";
  /** Description of offered item */
  descriptionOfItem = "";
  /** Path to redirect after completion */
  destinationPathAfter = "";
  /** Controls back button visibility */
  hideBackButton = false;
  /** Additional message to display */
  message = "";
  /** Flag for project assignment */
  offeredToProject = false;
  /** Flag for recipient assignment */
  offeredToRecipient = false;
  /** DID of offer creator */
  offererDid: string | undefined;
  /** Offer ID for editing */
  offerId = "";
  /** Previous offer data for editing */
  prevCredToEdit?: GenericCredWrapper<OfferClaim>;
  /** Project ID if offer is for project */
  projectId = "";
  /** Project name display */
  projectName = "a project";
  /** Recipient DID if offer is for person */
  recipientDid = "";
  /** Recipient name display */
  recipientName = "";
  /** Advanced features visibility flag */
  showGeneralAdvanced = false;
  /** Unit type for offer amount */
  unitCode = "HUR";
  /** Expiration date input */
  validThroughDateInput = "";

  /** Utility library reference */
  libsUtil = libsUtil;

  /**
   * Component lifecycle hook that initializes notification helpers
   */
  created() {
    this.notify = createNotifyHelpers(this.$notify);
  }

  /**
   * Computed property for recipient display name
   * Streamlines template logic for recipient/project display
   */
  get recipientDisplayName() {
    return this.offeredToProject
      ? this.projectName
      : this.offeredToRecipient
        ? this.recipientName
        : "someone not named";
  }

  /**
   * Computed property for project assignment label
   * Streamlines template logic for project checkbox label
   */
  get projectAssignmentLabel() {
    return this.projectId
      ? `This is offered to ${this.projectName}`
      : "No project was chosen";
  }

  /**
   * Computed property for recipient assignment label
   * Streamlines template logic for recipient checkbox label
   */
  get recipientAssignmentLabel() {
    return this.recipientDid
      ? `This is offered to ${this.recipientName}`
      : "No named individual recipient was chosen.";
  }

  /**
   * Component lifecycle hook that initializes the offer form
   *
   * Workflow:
   * 1. Extracts previous offer data if editing
   * 2. Sets initial form values from route or previous offer
   * 3. Loads account settings and contacts
   * 4. Retrieves project information if needed
   * 5. Sets offer assignment flags
   *
   * @throws Will not throw but shows notifications
   * @emits Notifications on loading errors
   */
  async mounted() {
    try {
      await this.loadPreviousOffer();
      await this.initializeFormValues();
      await this.loadAccountSettings();
      await this.loadRecipientInfo();
      await this.loadProjectInfo();
    } catch (err: unknown) {
      logger.error("Error in mounted:", err);
      this.notify.error(
        (err as Error)?.message || NOTIFY_OFFER_ERROR_LOADING.message,
        TIMEOUTS.LONG,
      );
    }
  }

  /**
   * Loads previous offer data if editing an existing offer
   * @throws Will not throw but shows notifications
   */
  private async loadPreviousOffer() {
    try {
      this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
        ? (JSON.parse(
            this.$route.query["prevCredToEdit"] as string,
          ) as GenericCredWrapper<OfferClaim>)
        : undefined;
    } catch (error: unknown) {
      this.notify.error(
        NOTIFY_OFFER_ERROR_PREVIOUS_RECORD.message,
        TIMEOUTS.LONG,
      );
    }
  }

  /**
   * Initializes form values from route params or previous offer
   */
  private async initializeFormValues() {
    const prevAmount =
      this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
    this.amountInput =
      (this.$route.query["amountInput"] as string) ||
      (prevAmount ? String(prevAmount) : "") ||
      this.amountInput;

    this.unitCode = ((this.$route.query["unitCode"] as string) ||
      this.prevCredToEdit?.claim?.includesObject?.unitCode ||
      this.unitCode) as string;

    this.descriptionOfCondition =
      this.prevCredToEdit?.claim?.description || this.descriptionOfCondition;

    this.descriptionOfItem =
      (this.$route.query["description"] as string) ||
      this.prevCredToEdit?.claim?.itemOffered?.description ||
      this.descriptionOfItem;

    this.destinationPathAfter =
      (this.$route.query["destinationPathAfter"] as string) || "";
    this.hideBackButton =
      (this.$route.query["hideBackButton"] as string) === "true";
    this.message = (this.$route.query["message"] as string) || "";

    // Set project info from previous offer or route
    let project;
    if (
      this.prevCredToEdit?.claim?.itemOffered?.isPartOf?.["@type"] ===
      "PlanAction"
    ) {
      project = this.prevCredToEdit?.claim?.itemOffered?.isPartOf;
    }
    this.projectId = ((this.$route.query["projectId"] as string) ||
      project?.identifier ||
      this.projectId) as string;
    this.projectName = ((this.$route.query["projectName"] as string) ||
      project?.name ||
      this.projectName) as string;

    this.recipientDid = ((this.$route.query["recipientDid"] as string) ||
      this.prevCredToEdit?.claim?.recipient?.identifier) as string;
    this.recipientName = (this.$route.query["recipientName"] as string) || "";

    this.validThroughDateInput =
      this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
  }

  /**
   * Loads account settings and updates component state
   * @throws Will not throw but logs errors
   */
  private async loadAccountSettings() {
    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.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
  }

  /**
   * Loads recipient information if recipient DID exists
   */
  private async loadRecipientInfo() {
    if (this.recipientDid && !this.recipientName) {
      const allContacts = await this.$getAllContacts();
      const allMyDids = await retrieveAccountDids();
      this.recipientName = didInfo(
        this.recipientDid,
        this.activeDid,
        allMyDids,
        allContacts,
      );
    }
    // Set assignment flags
    this.offeredToProject = !!this.projectId;
    this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
  }

  /**
   * Loads project information if project ID exists
   */
  private async loadProjectInfo() {
    if (this.projectId && !this.projectName) {
      const project = await getPlanFromCache(
        this.projectId,
        this.axios,
        this.apiServer,
        this.activeDid,
      );
      this.projectName = project?.name
        ? "the project: " + project.name
        : "a project";
    }
  }

  /**
   * Changes the unit type for the offer amount
   *
   * Cycles through available unit types in UNIT_SHORT.
   * Updates display and internal state.
   */
  changeUnitCode() {
    const units = Object.keys(this.libsUtil.UNIT_SHORT);
    const index = units.indexOf(this.unitCode);
    this.unitCode = units[(index + 1) % units.length];
  }

  /**
   * Increments the offer amount by 1
   *
   * Handles string to number conversion and updates display.
   */
  increment() {
    this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
  }

  /**
   * Decrements the offer amount by 1
   *
   * Prevents negative values and handles string to number conversion.
   */
  decrement() {
    this.amountInput = `${Math.max(
      0,
      (parseFloat(this.amountInput) || 1) - 1,
    )}`;
  }

  /**
   * Handles cancellation of offer creation/editing
   *
   * Workflow:
   * 1. Checks for destination path
   * 2. Navigates to destination or previous page
   *
   * @emits Router navigation
   */
  cancel() {
    if (this.destinationPathAfter) {
      (this.$router as Router).push({ path: this.destinationPathAfter });
    } else {
      (this.$router as Router).back();
    }
  }

  /**
   * Handles back button navigation
   *
   * @emits Router navigation to previous page
   */
  cancelBack() {
    (this.$router as Router).back();
  }

  /**
   * Validates and initiates offer submission
   *
   * Workflow:
   * 1. Validates active DID exists
   * 2. Checks for negative amounts
   * 3. Ensures description or amount exists
   * 4. Shows processing notification
   * 5. Calls recordOffer for submission
   *
   * @throws Will not throw but shows notifications
   * @emits Notifications for validation errors or processing
   */
  async confirm() {
    if (!this.activeDid) {
      this.notify.error(
        NOTIFY_OFFER_ERROR_NO_IDENTIFIER.message,
        TIMEOUTS.SHORT,
      );
      return;
    }
    if (parseFloat(this.amountInput) < 0) {
      this.notify.error(
        NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT.message,
        TIMEOUTS.SHORT,
      );
      return;
    }
    if (!this.descriptionOfItem && !parseFloat(this.amountInput)) {
      const message = NOTIFY_OFFER_ERROR_NO_DESCRIPTION.message.replace(
        "{unit}",
        this.libsUtil.UNIT_LONG[this.unitCode],
      );
      this.notify.error(message, TIMEOUTS.SHORT);
      return;
    }

    this.notify.toast("", NOTIFY_OFFER_PROCESSING.message, TIMEOUTS.SHORT);

    // this is asynchronous, but we don't need to wait for it to complete
    await this.recordOffer();
  }

  /**
   * Notifies user about project assignment restrictions
   *
   * Shows appropriate error message based on:
   * - Missing project ID
   * - Conflict with recipient assignment
   *
   * @emits Notification with error message
   */
  notifyUserOfProject() {
    if (!this.projectId) {
      this.notify.warning(
        NOTIFY_OFFER_ERROR_PROJECT_ASSIGNMENT.message,
        TIMEOUTS.STANDARD,
      );
    } else {
      // must be because offeredToRecipient is true
      this.notify.warning(
        NOTIFY_OFFER_ERROR_PROJECT_RECIPIENT_CONFLICT.message,
        TIMEOUTS.STANDARD,
      );
    }
  }

  /**
   * Notifies user about recipient assignment restrictions
   *
   * Shows appropriate error message based on:
   * - Missing recipient DID
   * - Conflict with project assignment
   *
   * @emits Notification with error message
   */
  notifyUserOfRecipient() {
    if (!this.recipientDid) {
      this.notify.warning(
        NOTIFY_OFFER_ERROR_RECIPIENT_ASSIGNMENT.message,
        TIMEOUTS.STANDARD,
      );
    } else {
      // must be because offeredToProject is true
      this.notify.warning(
        NOTIFY_OFFER_ERROR_RECIPIENT_PROJECT_CONFLICT.message,
        TIMEOUTS.STANDARD,
      );
    }
  }

  /**
   * Records the offer to the server
   *
   * Workflow:
   * 1. Determines if editing existing or creating new
   * 2. Prepares offer data with assignments
   * 3. Submits to server via appropriate method
   * 4. Handles success/error responses
   * 5. Navigates on success
   *
   * @throws Will not throw but shows notifications
   * @emits Notifications for success/failure
   * @emits Router navigation on success
   */
  public async recordOffer() {
    try {
      const recipientDid = this.offeredToRecipient
        ? this.recipientDid
        : undefined;
      const projectId = this.offeredToProject ? this.projectId : undefined;
      let result;
      if (this.prevCredToEdit) {
        // don't create from a blank one in case some properties were set from a different interface
        result = await editAndSubmitOffer(
          this.axios,
          this.apiServer,
          this.prevCredToEdit,
          this.activeDid,
          this.descriptionOfItem,
          parseFloat(this.amountInput),
          this.unitCode,
          this.descriptionOfCondition,
          this.validThroughDateInput,
          recipientDid,
          projectId,
        );
      } else {
        result = await createAndSubmitOffer(
          this.axios,
          this.apiServer,
          this.activeDid,
          this.descriptionOfItem,
          parseFloat(this.amountInput),
          this.unitCode,
          this.descriptionOfCondition,
          this.validThroughDateInput,
          recipientDid,
          projectId,
        );
      }

      if (!result.success) {
        const errorMessage = this.getCreationErrorMessage(result);
        logger.error("Error with offer creation result:", result);
        this.notify.error(
          errorMessage || NOTIFY_OFFER_ERROR_CREATION.message,
          TIMEOUTS.LONG,
        );
      } else {
        this.notify.success(
          NOTIFY_OFFER_SUCCESS_RECORDED.message,
          TIMEOUTS.LONG,
        );
        localStorage.removeItem("imageUrl");
        if (this.destinationPathAfter) {
          (this.$router as Router).push({ path: this.destinationPathAfter });
        } else {
          (this.$router as Router).back();
        }
      }
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (error: any) {
      logger.error("Error with offer recordation caught:", error);
      const errorMessage =
        error.userMessage ||
        error.response?.data?.error?.message ||
        NOTIFY_OFFER_ERROR_RECORDATION.message;
      this.notify.error(errorMessage, TIMEOUTS.LONG);
    }
  }

  /**
   * Constructs offer parameters for raw editing
   *
   * Creates a JSON string containing:
   * - Offer details
   * - Assignments
   * - Conditions
   * - Expiration
   *
   * @returns JSON string of offer parameters
   */
  constructOfferParam() {
    const recipientDid = this.offeredToRecipient
      ? this.recipientDid
      : undefined;
    const projectId = this.offeredToProject ? this.projectId : undefined;
    const offerClaim = hydrateOffer(
      this.prevCredToEdit?.claim as OfferClaim,
      this.activeDid,
      recipientDid,
      this.descriptionOfItem,
      parseFloat(this.amountInput),
      this.unitCode,
      this.descriptionOfCondition,
      projectId,
      this.validThroughDateInput,
      this.prevCredToEdit?.id as string,
    );
    const claimStr = JSON.stringify(offerClaim);
    return claimStr;
  }

  /**
   * Checks if server response indicates an error
   *
   * @param result Response data from server
   * @returns true if response indicates error
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  isCreationError(result: any) {
    return result.status !== 201 || result.data?.error;
  }

  /**
   * Extracts error message from server response
   *
   * @param result Server response object
   * @returns Best available error message
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  getCreationErrorMessage(result: any) {
    return (
      result.error?.userMessage ||
      result.error?.error ||
      result.response?.data?.error?.message
    );
  }

  /**
   * Shows privacy information dialog
   *
   * Displays standard privacy message about data sharing.
   *
   * @emits Notification with privacy message
   */
  explainData() {
    this.notify.success(NOTIFY_OFFER_PRIVACY_INFO.message, TIMEOUTS.VERY_LONG);
  }
}
</script>
