/**
 * Customer Portal JavaScript
 *
 * Handles subscription actions in the My Account area.
 *
 * @package ArraySubs
 */

import "@scss/customer-portal/index.scss";
import { buildRestUrl } from "@libs/url";
import {
  confirmDialog,
  portalToast,
  promptDialog,
  setButtonLoading,
} from "./customer-portal/uiHelpers";

(function ($) {
  "use strict";

  // Bail if our localized data is not available
  if (typeof window.arraySubsPortal === "undefined") {
    return;
  }

  const { nonce, i18n } = window.arraySubsPortal;

  function getApiBaseUrl() {
    const base =
      window.arraySubsSubscription?.restUrl ||
      window.arraySubsPortal?.apiBaseUrl ||
      "";

    if (!base) {
      return "";
    }

    return base.endsWith("/") ? base : `${base}/`;
  }

  function buildApiUrl(path) {
    const base = getApiBaseUrl();
    const cleanPath = String(path || "").replace(/^\/+/, "");

    return buildRestUrl(base, cleanPath);
  }

  async function requestPortalJson(url, options = {}) {
    const headers = new Headers(options.headers || {});
    headers.set("X-WP-Nonce", nonce);

    if (options.body && !headers.has("Content-Type")) {
      headers.set("Content-Type", "application/json");
    }

    const response = await fetch(url, {
      method: options.method || "POST",
      credentials: "same-origin",
      ...options,
      headers,
    });
    const text = await response.text();
    let payload = {};

    if (text) {
      try {
        payload = JSON.parse(text);
      } catch (e) {
        payload = { message: text };
      }
    }

    if (!response.ok) {
      const error = new Error(
        payload?.message || response.statusText || "Request failed",
      );
      error.status = response.status;
      error.payload = payload;
      throw error;
    }

    return payload;
  }

  function initSubscriptionsMenuBadge() {
    const subscriptionCount = parseInt(
      window.arraySubsPortal?.subscriptionCount,
      10,
    );

    if (!subscriptionCount || subscriptionCount <= 0) {
      return;
    }

    const menuItem = document.querySelector(
      ".woocommerce-MyAccount-navigation-link--subscriptions a",
    );

    if (!menuItem || menuItem.querySelector(".arraysubs-menu-badge")) {
      return;
    }

    const badge = document.createElement("span");
    badge.className = "arraysubs-menu-badge";
    badge.textContent = String(subscriptionCount);
    menuItem.appendChild(badge);
  }

  /**
   * Cancel Modal Handler
   */
  class CancelModal {
    constructor() {
      this.modal = $("#arraysubs-cancel-modal");
      this.reasonSelect = $("#cancel-reason");
      this.otherReasonWrapper = $("#cancel-other-reason-wrapper");
      this.otherReasonTextarea = $("#cancel-other-reason");
      this.confirmBtn = this.modal.find(".arraysubs-modal__confirm-btn");
      this.cancelBtn = this.modal.find(".arraysubs-modal__cancel-btn");
      this.closeBtn = this.modal.find(".arraysubs-modal__close");
      this.backdrop = this.modal.find(".arraysubs-modal__backdrop");

      this.cancelUrl = "";
      this.subscriptionId = "";
      this.triggerButton = null;

      this.init();
    }

    init() {
      // Open modal when cancel button clicked
      $(document).on("click", ".arraysubs-cancel-subscription", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.cancelUrl = $btn.data("cancel-url");
        this.subscriptionId = $btn.data("subscription-id");
        this.triggerButton = $btn;
        this.open();
      });

      // Close modal events
      this.closeBtn.on("click", () => this.close());
      this.cancelBtn.on("click", () => this.close());
      this.backdrop.on("click", () => this.close());

      // Close on ESC key
      $(document).on("keydown", (e) => {
        if (e.key === "Escape" && this.modal.is(":visible")) {
          this.close();
        }
      });

      // Show/hide other reason textarea
      this.reasonSelect.on("change", () => {
        if (this.reasonSelect.val() === "other") {
          this.otherReasonWrapper.slideDown(200);
        } else {
          this.otherReasonWrapper.slideUp(200);
        }

        this.updateConfirmState();
      });

      // Confirm cancellation
      this.confirmBtn.on("click", () => this.confirm());
    }

    open() {
      // Reset form
      this.reasonSelect.val("");
      this.otherReasonTextarea.val("");
      this.otherReasonWrapper.hide();

      // Clear any leftover spinner/loading state from a previous attempt.
      this.confirmBtn.removeData("arraysubs-original-html");
      this.confirmBtn
        .prop("disabled", false)
        .removeClass("arraysubs-btn--loading")
        .text(
          window.arraySubsSubscription?.i18n?.continueCancelFlow ||
            i18n.continue_action ||
            "Continue",
        );
      this.updateConfirmState();

      // Hide any leftover inline error.
      this.modal.find(".arraysubs-modal__inline-error").hide();

      // Show modal with animation
      this.modal.fadeIn(200);

      // Focus trap and accessibility
      this.modal.find(".arraysubs-modal__select").focus();

      // Prevent body scroll
      $("body").addClass("arraysubs-modal-open");
    }

    close() {
      this.modal.fadeOut(200);
      $("body").removeClass("arraysubs-modal-open");

      // Return focus to trigger button
      if (this.triggerButton) {
        this.triggerButton.focus();
      }
    }

    isReasonRequired() {
      return Boolean(
        window.arraySubsSubscription?.requireReason ||
          this.reasonSelect.prop("required"),
      );
    }

    updateConfirmState() {
      if (!this.confirmBtn.length) {
        return;
      }

      const missingRequiredReason =
        this.isReasonRequired() && !this.reasonSelect.val();

      this.confirmBtn.prop("disabled", missingRequiredReason);

      if (!missingRequiredReason) {
        this.modal.find(".arraysubs-modal__inline-error").hide();
      }
    }

    confirm() {
      const reason = this.reasonSelect.val();
      const otherReason = this.otherReasonTextarea.val();
      const requireReason = this.isReasonRequired();

      // Validate reason if required — show inline message rather than alert.
      if (requireReason && !reason) {
        const reasonRequiredMsg =
          window.arraySubsSubscription?.i18n?.reasonRequired ||
          i18n?.reasonRequired ||
          "Please select a reason for cancelling.";
        let $err = this.modal.find(".arraysubs-modal__inline-error");
        if (!$err.length) {
          $err = $('<p class="arraysubs-modal__inline-error" role="alert" style="margin:8px 0 0;color:#b91c1c;font-size:13px;"></p>');
          this.reasonSelect.closest(".arraysubs-modal__form").append($err);
        }
        $err.text(reasonRequiredMsg).show();
        this.reasonSelect.trigger("focus");
        return;
      }
      this.modal.find(".arraysubs-modal__inline-error").hide();

      // Disable button and show spinner state while AJAX runs. The modal
      // itself is the explicit confirmation step — no extra native confirm.
      setButtonLoading(this.confirmBtn, true, i18n?.cancelling || "Cancelling…");

      $.ajax({
        url: this.cancelUrl,
        method: "POST",
        beforeSend: (xhr) => {
          xhr.setRequestHeader("X-WP-Nonce", nonce);
          xhr.setRequestHeader("Content-Type", "application/json");
        },
        data: JSON.stringify({
          reason: reason,
          other_reason: otherReason,
        }),
        success: (response) => {
          const message =
            response?.message ||
            i18n?.cancelled_success ||
            "Subscription cancelled.";
          portalToast(message, "success");
          // Brief delay so the customer sees the toast before reload.
          setTimeout(() => window.location.reload(), 600);
        },
        error: (xhr) => {
          const message =
            xhr.responseJSON && xhr.responseJSON.message
              ? xhr.responseJSON.message
              : i18n?.cancelled_error || "Failed to cancel subscription.";

          portalToast(message, "error");
          setButtonLoading(this.confirmBtn, false);
        },
      });
    }
  }

  /**
   * Plan Switch Modal Handler
   */
  class PlanSwitchModal {
    constructor() {
      this.modal = $("#arraysubs-switch-modal");
      if (!this.modal.length) return;

      this.optionsContainer = $("#switch-options-container");
      this.previewContainer = $("#switch-preview-container");
      this.confirmBtn = $("#confirm-switch-btn");
      this.cancelBtn = this.modal.find(".arraysubs-modal__cancel-btn");
      this.closeBtn = this.modal.find(".arraysubs-modal__close");
      this.backdrop = this.modal.find(".arraysubs-modal__backdrop");

      this.subscriptionId = "";
      this.selectedPlan = null;
      this.switchData = null;

      this.init();
    }

    init() {
      // Open modal when switch button clicked
      $(document).on("click", ".arraysubs-switch-plan", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.subscriptionId = $btn.data("subscription-id");
        this.loadOptions($btn.data("switch-options-url"));
        this.open();
      });

      // Close modal events
      this.closeBtn.on("click", () => this.close());
      this.cancelBtn.on("click", () => this.close());
      this.backdrop.on("click", () => this.close());

      // Close on ESC key
      $(document).on("keydown", (e) => {
        if (e.key === "Escape" && this.modal.is(":visible")) {
          this.close();
        }
      });

      // Select plan option
      $(document).on("click", ".arraysubs-switch-option", (e) => {
        const $option = $(e.currentTarget);
        this.selectPlan($option);
      });

      // Tab switching
      $(document).on("click", ".arraysubs-tabs__nav", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        const targetId = $btn.data("target");

        // Toggle active class on buttons
        $btn.addClass("active").siblings().removeClass("active");

        // Toggle active class on panes
        $("#" + targetId)
          .addClass("active")
          .siblings()
          .removeClass("active");
      });

      // Confirm switch
      this.confirmBtn.on("click", () => this.confirm());
    }

    open() {
      this.selectedPlan = null;
      this.switchData = null;
      this.previewContainer.hide();
      this.confirmBtn.hide();
      this.modal.fadeIn(200);
      $("body").addClass("arraysubs-modal-open");
    }

    close() {
      this.modal.fadeOut(200);
      $("body").removeClass("arraysubs-modal-open");
    }

    async requestJson(url, options = {}) {
      const headers = new Headers(options.headers || {});
      headers.set("X-WP-Nonce", nonce);

      if (options.body && !headers.has("Content-Type")) {
        headers.set("Content-Type", "application/json");
      }

      const response = await fetch(url, {
        method: options.method || "POST",
        credentials: "same-origin",
        ...options,
        headers,
      });
      const text = await response.text();
      let payload = {};

      if (text) {
        try {
          payload = JSON.parse(text);
        } catch (e) {
          payload = { message: text };
        }
      }

      if (!response.ok) {
        const error = new Error(
          payload?.message || response.statusText || "Request failed",
        );
        error.status = response.status;
        error.payload = payload;
        throw error;
      }

      return payload;
    }

    async loadOptions(url) {
      this.optionsContainer.html(
        '<p class="arraysubs-switch-loading">' +
          (window.arraySubsSubscription?.i18n?.loading || "Loading...") +
          "</p>",
      );

      try {
        const response = await this.requestJson(url);
        // Flatten options object into array (API returns {upgrades: [], downgrades: [], crossgrades: []})
        // Note: API returns response.content (not response.data)
        const optionsData =
          response.content?.options || response.data?.options || {};

        const upgrades = optionsData.upgrades || [];
        const downgrades = optionsData.downgrades || [];
        const crossgrades = optionsData.crossgrades || [];

        // Tab 1: Upgrade/Downgrade
        const mainTabItems = [...upgrades, ...downgrades];

        // Tab 2: Crossgrades (Others)
        const otherTabItems = [...crossgrades];

        // If no items at all
        if (mainTabItems.length === 0 && otherTabItems.length === 0) {
          this.optionsContainer.html(
            '<p class="arraysubs-switch-empty">' +
              (window.arraySubsSubscription?.i18n?.noPlansAvailable ||
                "No plan changes available for this subscription.") +
              "</p>",
          );
          return;
        }

        let html = '<div class="arraysubs-tabs">';

        // Only show tab headers if we have BOTH types of items
        // Or if we strictly follow "tab" design even if one is empty?
        // Requirement: "if no products are set, hide the whole tab" -> implying individual tab.

        const showMainTab = mainTabItems.length > 0;
        const showOtherTab = otherTabItems.length > 0;

        // Headers
        html += '<div class="arraysubs-tabs__header">';

        if (showMainTab) {
          html +=
            '<button type="button" class="arraysubs-tabs__nav active" data-target="tab-main">' +
            (window.arraySubsSubscription?.i18n?.upgradeDowngrade ||
              "Upgrade/Downgrade") +
            "</button>";
        }

        if (showOtherTab) {
          const activeClass = !showMainTab ? "active" : "";
          const label = window.arraySubsSubscription?.i18n?.others || "Others";
          html += `<button type="button" class="arraysubs-tabs__nav ${activeClass}" data-target="tab-others">${label}</button>`;
        }
        html += "</div>"; // End header

        // Content
        html += '<div class="arraysubs-tabs__content">';

        // Main Tab Pane
        if (showMainTab) {
          html +=
            '<div class="arraysubs-tabs__pane active" id="tab-main"><div class="arraysubs-switch-options__list">';
          mainTabItems.forEach((opt) => {
            html += this.renderOption(opt);
          });
          html += "</div></div>";
        }

        // Other Tab Pane
        if (showOtherTab) {
          const activeClass = !showMainTab ? "active" : "";
          html += `<div class="arraysubs-tabs__pane ${activeClass}" id="tab-others"><div class="arraysubs-switch-options__list">`;
          otherTabItems.forEach((opt) => {
            html += this.renderOption(opt);
          });
          html += "</div></div>";
        }

        html += "</div></div>"; // End content & tabs wrapper

        this.optionsContainer.html(html);
      } catch (error) {
        this.optionsContainer.html(
          '<p class="arraysubs-switch-error">' +
            (window.arraySubsSubscription?.i18n?.loadError ||
              "Failed to load plan options. Please try again.") +
            "</p>",
        );
      }
    }

    renderOption(opt) {
      let switchLabel;
      let typeClass = opt.switch_type; // 'upgrade', 'downgrade', 'crossgrade'

      if (opt.switch_type === "crossgrade") {
        switchLabel = window.arraySubsSubscription?.i18n?.change || "Change";
        // Maybe add a distinct class for styling crossgrade label if needed
      } else {
        switchLabel =
          opt.switch_type === "upgrade"
            ? window.arraySubsSubscription?.i18n?.upgrade || "Upgrade"
            : window.arraySubsSubscription?.i18n?.downgrade || "Downgrade";
      }

      // Use product_name with fallback to name
      const productName = opt.product_name || opt.name || "Unknown Plan";
      const productUrl = opt.product_url || "";
      const viewDetailsText =
        window.arraySubsSubscription?.i18n?.viewDetails || "View Details";

      // Build product name with optional link
      let productNameHtml = this.escapeHtml(productName);
      let viewDetailsLink = "";
      if (productUrl) {
        viewDetailsLink = `<a href="${this.escapeHtml(
          productUrl,
        )}" class="arraysubs-switch-option__link" target="_blank" rel="noopener noreferrer">${viewDetailsText}</a>`;
      }

      return `
        <div class="arraysubs-switch-option" data-product-id="${
          opt.product_id
        }" data-switch-type="${opt.switch_type}">
          <div class="arraysubs-switch-option__info">
            <h4 class="arraysubs-switch-option__name">${productNameHtml}</h4>
            ${viewDetailsLink}
            <p class="arraysubs-switch-option__price">${
              opt.formatted_price || ""
            }</p>
          </div>
          <div class="arraysubs-switch-option__action">
            <span class="arraysubs-switch-option__type arraysubs-switch-option__type--${typeClass}">${switchLabel}</span>
            <button type="button" class="button">${
              window.arraySubsSubscription?.i18n?.select || "Select"
            }</button>
          </div>
        </div>
      `;
    }

    selectPlan($option) {
      $(".arraysubs-switch-option").removeClass(
        "arraysubs-switch-option--selected",
      );
      $option.addClass("arraysubs-switch-option--selected");

      this.selectedPlan = {
        productId: $option.data("product-id"),
        switchType: $option.data("switch-type"),
      };

      this.loadPreview();
    }

    async loadPreview() {
      const previewUrl = buildApiUrl(
        `subscriptions/${this.subscriptionId}/switch-preview`,
      );

      this.previewContainer
        .show()
        .find(".arraysubs-switch-preview__details")
        .html(
          '<p class="arraysubs-switch-loading">' +
            (window.arraySubsSubscription?.i18n?.loading || "Loading...") +
            "</p>",
        );

      try {
        const response = await this.requestJson(previewUrl, {
          body: JSON.stringify({
            new_product_id: this.selectedPlan.productId,
          }),
        });
        const data = this.normalizeApiPayload(response);
        this.switchData = data;

        const formatted = data?.formatted || data?.proration?.formatted || {};
        const currency =
          data?.currency ||
          data?.proration?.currency ||
          window.arraySubsSubscription?.currency ||
          window.arraySubsPortal?.currency ||
          "USD";

        const creditAmount = Number(
          data?.credit_amount ?? data?.proration?.credit ?? data?.credit ?? 0,
        );
        const chargeAmount = Number(
          data?.charge_amount ?? data?.proration?.charge ?? data?.charge ?? 0,
        );
        const netAmount = Number(
          data?.net_amount ?? data?.proration?.net ?? data?.net ?? 0,
        );
        const switchFeeAmount = Number(
          data?.switch_fee ?? data?.proration?.switch_fee ?? 0,
        );

        const switchFeeRow =
          switchFeeAmount > 0
            ? `
              <tr>
                <td>Switch fee:</td>
                <td>${
                  formatted?.switch_fee ||
                  this.formatCurrency(switchFeeAmount, currency)
                }</td>
              </tr>`
            : "";

        let html = `
            <table class="arraysubs-switch-preview__table">
              <tr>
                <td>Credit for unused time:</td>
                <td>${
                  formatted?.credit ||
                  this.formatCurrency(creditAmount, currency)
                }</td>
              </tr>
              <tr>
                <td>Charge for new plan:</td>
                <td>${
                  formatted?.charge ||
                  this.formatCurrency(chargeAmount, currency)
                }</td>
              </tr>${switchFeeRow}
              <tr class="arraysubs-switch-preview__total">
                <td><strong>Amount due:</strong></td>
                <td><strong>${
                  formatted?.net || this.formatCurrency(netAmount, currency)
                }</strong></td>
              </tr>
            </table>
          `;

        const refundAmount = Number(
          data?.refund_amount ?? data?.proration?.refund ?? 0,
        );

        if (refundAmount > 0) {
          html += `<p class="arraysubs-switch-preview__refund">You will receive a credit of ${
            formatted?.refund || this.formatCurrency(refundAmount, currency)
          }</p>`;
        }

        this.previewContainer
          .find(".arraysubs-switch-preview__details")
          .html(html);
        this.confirmBtn.show();
      } catch (error) {
        this.previewContainer
          .find(".arraysubs-switch-preview__details")
          .html(
            '<p class="arraysubs-switch-error">Failed to calculate proration. Please try again.</p>',
          );
      }
    }

    normalizeApiPayload(response) {
      if (!response || typeof response !== "object") {
        return {};
      }

      if (response.content && typeof response.content === "object") {
        if (
          response.content.content &&
          typeof response.content.content === "object"
        ) {
          return response.content.content;
        }
        return response.content;
      }

      if (response.data && typeof response.data === "object") {
        if (
          response.data.content &&
          typeof response.data.content === "object"
        ) {
          return response.data.content;
        }
        return response.data;
      }

      return response;
    }

    formatCurrency(amount, currencyCode) {
      const value = Number.isFinite(amount) ? amount : 0;
      const safeCurrency = currencyCode || "USD";

      try {
        return new Intl.NumberFormat(undefined, {
          style: "currency",
          currency: safeCurrency,
        }).format(value);
      } catch (e) {
        return `${value.toFixed(2)} ${safeCurrency}`;
      }
    }

    async confirm(replacePending = false) {
      if (!replacePending) {
        const confirmed = await confirmDialog({
          title:
            window.arraySubsSubscription?.i18n?.confirmSwitchTitle ||
            "Confirm Plan Change",
          message:
            window.arraySubsSubscription?.i18n?.confirmSwitch ||
            "Are you sure you want to change your plan?",
          confirmText:
            window.arraySubsSubscription?.i18n?.confirmSwitchAction ||
            "Change Plan",
          cancelText:
            window.arraySubsSubscription?.i18n?.cancel || "Cancel",
          variant: "info",
        });
        if (!confirmed) return;
      }

      const switchUrl = buildApiUrl(
        `subscriptions/${this.subscriptionId}/switch`,
      );

      setButtonLoading(
        this.confirmBtn,
        true,
        window.arraySubsSubscription?.i18n?.loading || "Processing…",
      );

      try {
        const response = await this.requestJson(switchUrl, {
          body: JSON.stringify({
            new_product_id: this.selectedPlan.productId,
            replace_pending: replacePending,
          }),
        });
        const data = response.content || response.data || {};

        if (data.requires_payment && data.checkout_url) {
          // Redirect to checkout for payment
          window.location.href = data.checkout_url;
          return;
        }

        portalToast(
          window.arraySubsSubscription?.i18n?.switchSuccess ||
            "Your plan has been changed successfully!",
          "success",
        );
        setTimeout(() => window.location.reload(), 600);
      } catch (error) {
        const payload = this.normalizeApiPayload(error.payload || {});
        if (
          !replacePending &&
          error.status === 409 &&
          payload?.requires_replace_confirmation
        ) {
          const target =
            payload?.pending_switch?.target_product_name || "another plan";
          const confirmReplace =
            window.arraySubsSubscription?.i18n?.replacePendingSwitchConfirm ||
            `You already have a pending switch to ${target}. Replace it?`;
          setButtonLoading(this.confirmBtn, false);

          const ok = await confirmDialog({
            title:
              window.arraySubsSubscription?.i18n?.replacePendingSwitchTitle ||
              "Replace Pending Switch",
            message: confirmReplace,
            confirmText:
              window.arraySubsSubscription?.i18n?.replacePendingSwitchAction ||
              "Replace",
            variant: "warning",
          });
          if (ok) this.confirm(true);
          return;
        }

        const message =
          payload?.message ||
          error.message ||
          window.arraySubsSubscription?.i18n?.switchError ||
          "Failed to change plan. Please try again.";
        portalToast(message, "error");
        setButtonLoading(this.confirmBtn, false);
      }
    }

    escapeHtml(text) {
      const div = document.createElement("div");
      div.textContent = text;
      return div.innerHTML;
    }
  }

  /**
   * Retention Offers Handler
   */
  class RetentionOffers {
    constructor() {
      this.modal = $("#arraysubs-retention-modal");
      if (!this.modal.length) return;

      this.title = this.modal.find("#retention-modal-title");
      this.intro = this.modal.find(".arraysubs-retention__intro");
      this.offersContainer = $("#retention-offers-container");
      this.skipBtn = this.modal.find(".arraysubs-retention__skip-btn");
      this.closeBtn = this.modal.find(".arraysubs-modal__close");
      this.backdrop = this.modal.find(".arraysubs-modal__backdrop");

      this.subscriptionId = "";
      this.selectedReason = "";
      this.selectedOtherReason = "";
      this.cancelModal = null;

      this.init();
    }

    init() {
      // Close modal events
      this.closeBtn.on("click", () => this.close());
      this.backdrop.on("click", () => this.close());

      // Keep subscription (close all modals)
      this.modal.on("click", ".arraysubs-retention__keep-btn", () =>
        this.keepSubscription(),
      );

      // Skip to cancellation
      this.skipBtn.on("click", () => this.skipToCancel());

      // Accept offer
      $(document).on("click", ".arraysubs-retention-offer__accept-btn", (e) => {
        const $offer = $(e.currentTarget).closest(".arraysubs-retention-offer");
        this.acceptOffer($offer);
      });

      // Close on ESC key
      $(document).on("keydown", (e) => {
        if (e.key === "Escape" && this.modal.is(":visible")) {
          this.close();
        }
      });
    }

    show(
      subscriptionId,
      cancelModal,
      selectedReason = "",
      selectedOtherReason = "",
    ) {
      this.subscriptionId = subscriptionId;
      this.cancelModal = cancelModal;
      this.selectedReason = selectedReason;
      this.selectedOtherReason = selectedOtherReason;
      this.resetModalCopy();
      this.loadOffers();
      this.modal.fadeIn(200);
      $("body").addClass("arraysubs-modal-open");
    }

    close() {
      this.modal.fadeOut(200);
      $("body").removeClass("arraysubs-modal-open");
    }

    keepSubscription() {
      this.close();
      // Also close the cancel modal
      if (this.cancelModal) {
        this.cancelModal.close();
      }
    }

    resetModalCopy() {
      const i18n = window.arraySubsSubscription?.i18n || {};

      this.title.text(i18n.retentionModalTitle || "Before You Go...");
      this.intro.text(
        i18n.retentionModalIntro ||
          "We'd hate to see you leave! Here are some options that might help:",
      );
    }

    setUnsupportedGatewayCopy() {
      const i18n = window.arraySubsSubscription?.i18n || {};

      this.title.text(
        i18n.unsupportedRetentionTitle || "Please confirm your cancellation",
      );
      this.intro.text(
        i18n.unsupportedRetentionIntro ||
          "No retention options are available for this subscription. You can keep your subscription or continue to cancel.",
      );
    }

    showNoOffersConfirmation() {
      const i18n = window.arraySubsSubscription?.i18n || {};

      if (window.arraySubsSubscription?.retentionOffersSupported === false) {
        this.setUnsupportedGatewayCopy();
      }

      this.offersContainer.html(`
        <div class="arraysubs-retention-no-offers">
          <p class="arraysubs-retention-no-offers__message">
            ${
              i18n.noOffersConfirmMessage ||
              "Are you sure you want to cancel your subscription?"
            }
          </p>
        </div>
      `);
      // Update footer buttons
      this.modal.find(".arraysubs-modal__footer").html(`
        <button type="button" class="button button-primary arraysubs-retention__keep-btn">
          ${i18n.keepSubscription || "Keep Subscription"}
        </button>
        <button type="button" class="button arraysubs-retention__confirm-cancel-btn">
          ${
            i18n.continueToCancel ||
            i18n.skipToCancel ||
            "No thanks, continue to cancel"
          }
        </button>
      `);
      // Bind confirm cancel button
      this.modal
        .find(".arraysubs-retention__confirm-cancel-btn")
        .on("click", () => {
          this.close();
          if (this.cancelModal) {
            this.cancelModal.confirm();
          }
        });
      // Re-bind keep subscription button (since we replaced footer)
      this.modal
        .find(".arraysubs-retention__keep-btn")
        .on("click", () => this.keepSubscription());
    }

    loadOffers() {
      if (window.arraySubsSubscription?.retentionOffersSupported === false) {
        this.showNoOffersConfirmation();
        return;
      }

      let url = buildApiUrl(
        `subscriptions/${this.subscriptionId}/retention-offers`,
      );

      this.offersContainer.html(
        '<p class="arraysubs-retention-loading">' +
          (window.arraySubsSubscription?.i18n?.loading || "Loading...") +
          "</p>",
      );

      $.ajax({
        url: url,
        method: "POST",
        beforeSend: (xhr) => {
          xhr.setRequestHeader("X-WP-Nonce", nonce);
          xhr.setRequestHeader("Content-Type", "application/json");
        },
        data: JSON.stringify({
          reason: this.selectedReason || "",
        }),
        success: (response) => {
          const offers = response.content || response.data || [];
          if (offers.length === 0) {
            // No offers available - show confirmation step in this modal
            this.showNoOffersConfirmation();
            return;
          }

          let html = '<div class="arraysubs-retention-offers__list">';

          offers.forEach((offer) => {
            html += this.renderOfferHtml(offer);
          });

          html += "</div>";
          this.offersContainer.html(html);
        },
        error: (xhr) => {
          const message =
            xhr.responseJSON?.message ||
            window.arraySubsSubscription?.i18n?.retentionLoadError ||
            "Unable to load retention offers. You can keep your subscription or continue to cancel.";

          this.offersContainer.html(`
            <div class="arraysubs-retention-error" role="alert">
              <p>${this.escapeHtml(message)}</p>
            </div>
          `);
        },
      });
    }

    /**
     * Fallback HTML rendering for offers when template is not available
     */
    renderOfferHtml(offer) {
      const i18n = window.arraySubsSubscription?.i18n || {};
      let actionButton = "";

      if (offer.type === "contact") {
        const contactUrl = offer.config?.contact_url || "#";
        const buttonText =
          offer.config?.button_text || i18n.contactSupport || "Contact Support";
        actionButton = `<a href="${this.escapeHtml(
          contactUrl,
        )}" class="button button-primary arraysubs-retention-offer__contact-btn" target="_blank" rel="noopener">${this.escapeHtml(
          buttonText,
        )}</a>`;
      } else {
        const acceptText = i18n.acceptOffer || "Accept Offer";
        actionButton = `<button type="button" class="button button-primary arraysubs-retention-offer__accept-btn">${this.escapeHtml(
          acceptText,
        )}</button>`;
      }

      return `
        <div class="arraysubs-retention-offer" data-offer-id="${
          offer.id
        }" data-offer-type="${offer.type}">
          <div class="arraysubs-retention-offer__content">
            <h4 class="arraysubs-retention-offer__title">${this.escapeHtml(
              offer.title || "",
            )}</h4>
            <p class="arraysubs-retention-offer__description">${this.escapeHtml(
              offer.description || "",
            )}</p>
          </div>
          <div class="arraysubs-retention-offer__action">
            ${actionButton}
          </div>
        </div>
      `;
    }

    acceptOffer($offer) {
      const offerId = $offer.data("offer-id");
      const offerType = $offer.data("offer-type");
      const $btn = $offer.find(".arraysubs-retention-offer__accept-btn");

      setButtonLoading(
        $btn,
        true,
        window.arraySubsSubscription?.i18n?.loading || "Processing…",
      );

      const url = buildApiUrl(
        `subscriptions/${this.subscriptionId}/retention-offers/accept`,
      );

      $.ajax({
        url: url,
        method: "POST",
        beforeSend: (xhr) => {
          xhr.setRequestHeader("X-WP-Nonce", nonce);
          xhr.setRequestHeader("Content-Type", "application/json");
        },
        data: JSON.stringify({
          offer_id: offerId,
          offer_type: offerType,
        }),
        success: () => {
          portalToast(
            window.arraySubsSubscription?.i18n?.offerAccepted ||
              "Offer accepted! Your subscription has been updated.",
            "success",
          );
          setTimeout(() => window.location.reload(), 600);
        },
        error: (xhr) => {
          const message =
            xhr.responseJSON?.message ||
            window.arraySubsSubscription?.i18n?.offerError ||
            "Failed to apply offer. Please try again.";
          portalToast(message, "error");
          setButtonLoading($btn, false);
        },
      });
    }

    skipToCancel() {
      this.close();

      // Continue directly to final cancellation confirmation.
      if (this.cancelModal) {
        setTimeout(() => {
          if (this.selectedReason) {
            this.cancelModal.reasonSelect.val(this.selectedReason);
            this.cancelModal.reasonSelect.trigger("change");
          }

          if (this.selectedReason === "other") {
            this.cancelModal.otherReasonWrapper.show();
          }

          if (this.selectedOtherReason) {
            this.cancelModal.otherReasonTextarea.val(this.selectedOtherReason);
          }

          this.cancelModal.confirm();
        }, 250);
      }
    }

    escapeHtml(text) {
      const div = document.createElement("div");
      div.textContent = text;
      return div.innerHTML;
    }
  }

  /**
   * Skip Renewal Handler
   */
  class SkipRenewal {
    constructor() {
      this.init();
    }

    init() {
      // Skip button
      $(document).on("click", ".arraysubs-skip-renewal", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.promptSkip($btn);
      });

      // Undo skip button
      $(document).on("click", ".arraysubs-undo-skip", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.undoSkip($btn);
      });
    }

    async promptSkip($btn) {
      const maxCycles = parseInt($btn.data("max-cycles")) || 1;

      let cyclesToSkip = 1;
      if (maxCycles > 1) {
        const value = await promptDialog({
          title: i18n?.skip_prompt_title || "Skip Renewal",
          label:
            i18n?.skip_prompt_label ||
            `How many billing cycles would you like to skip?`,
          message:
            i18n?.skip_prompt_help ||
            `Enter a number between 1 and ${maxCycles}.`,
          inputType: "number",
          min: 1,
          max: maxCycles,
          defaultValue: "1",
          confirmText: i18n?.skip_prompt_action || "Continue",
          validate: (val) => {
            const n = parseInt(val, 10);
            if (isNaN(n) || n < 1 || n > maxCycles) {
              return (
                i18n?.skip_invalid_cycles ||
                `Please enter a number between 1 and ${maxCycles}.`
              );
            }
            return null;
          },
        });
        if (value === null) return;
        cyclesToSkip = parseInt(value, 10);
      }

      await confirmDialog({
        title: i18n?.skip_confirm_title || "Skip Renewal",
        message:
          i18n?.skip_confirm ||
          `Skip ${cyclesToSkip} billing cycle(s)? Your subscription will remain active.`,
        confirmText: i18n?.skip_confirm_action || "Skip",
        variant: "info",
        loadingText: i18n?.processing || "Processing…",
        onConfirm: () => this.processSkip($btn, cyclesToSkip),
      });
    }

    processSkip($btn, cycles) {
      const skipUrl = $btn.data("skip-url");

      return requestPortalJson(skipUrl, {
        method: "POST",
        body: JSON.stringify({
          cycles: cycles,
        }),
      })
        .then((response) => {
          const message =
            response?.message ||
            i18n?.skip_success ||
            "Billing cycle(s) skipped successfully!";
          portalToast(message, "success");
          setTimeout(() => window.location.reload(), 600);
          return response;
        })
        .catch((error) => {
          throw new Error(
            error?.payload?.message ||
              error?.message ||
              i18n?.skip_error ||
              "Failed to skip renewal. Please try again.",
          );
        });
    }

    async undoSkip($btn) {
      await confirmDialog({
        title: i18n?.undo_skip_title || "Undo Skip",
        message:
          i18n?.undo_skip_confirm ||
          "Undo the skip and restore your original renewal date?",
        confirmText: i18n?.undo_skip_action || "Undo Skip",
        variant: "info",
        loadingText: i18n?.processing || "Processing…",
        onConfirm: () => this.processUndoSkip($btn),
      });
    }

    processUndoSkip($btn) {
      const undoUrl = $btn.data("undo-url");

      return new Promise((resolve, reject) => {
        $.ajax({
          url: undoUrl,
          method: "POST",
          beforeSend: (xhr) => {
            xhr.setRequestHeader("X-WP-Nonce", nonce);
          },
          success: (response) => {
            const message =
              response?.message ||
              i18n?.undo_skip_success ||
              "Skip undone. Original renewal date restored.";
            portalToast(message, "success");
            setTimeout(() => window.location.reload(), 600);
            resolve(response);
          },
          error: (xhr) => {
            const message =
              xhr.responseJSON?.message ||
              i18n?.undo_skip_error ||
              "Failed to undo skip. Please try again.";
            reject(new Error(message));
          },
        });
      });
    }
  }

  /**
   * Reactivate Subscription Handler
   *
   * Reactivates cancelled or expired subscriptions when the customer
   * clicks the "Reactivate Subscription" button rendered on the
   * single subscription view.
   */
  class ReactivateSubscription {
    constructor() {
      this.init();
    }

    init() {
      $(document).on("click", ".arraysubs-reactivate-subscription", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.confirmReactivate($btn);
      });
    }

    async confirmReactivate($btn) {
      const confirmMessage =
        window.arraySubsSubscription?.i18n?.reactivateConfirm ||
        i18n?.reactivate_confirm ||
        "Reactivate this subscription? You will be billed again on the next renewal date.";

      const ok = await confirmDialog({
        title:
          window.arraySubsSubscription?.i18n?.reactivateTitle ||
          "Reactivate Subscription",
        message: confirmMessage,
        confirmText:
          window.arraySubsSubscription?.i18n?.reactivateAction ||
          "Reactivate",
        variant: "info",
      });
      if (!ok) return;

      const reactivateUrl = $btn.data("reactivate-url");
      if (!reactivateUrl) {
        return;
      }

      setButtonLoading($btn, true, i18n?.processing || "Processing…");

      $.ajax({
        url: reactivateUrl,
        method: "POST",
        beforeSend: (xhr) => {
          xhr.setRequestHeader("X-WP-Nonce", nonce);
        },
        success: (response) => {
          const message =
            response?.message ||
            window.arraySubsSubscription?.i18n?.reactivateSuccess ||
            i18n?.reactivate_success ||
            "Subscription reactivated successfully!";
          portalToast(message, "success");
          setTimeout(() => window.location.reload(), 600);
        },
        error: (xhr) => {
          const message =
            xhr.responseJSON?.message ||
            window.arraySubsSubscription?.i18n?.reactivateError ||
            i18n?.reactivate_error ||
            "Failed to reactivate subscription. Please try again.";
          portalToast(message, "error");
          setButtonLoading($btn, false);
        },
      });
    }
  }

  /**
   * Cancel Pending (deferred) Plan Switch
   *
   * Removes a pending APPLY_AT_RENEWAL switch before the next renewal
   * fires. The customer's current plan continues unchanged.
   */
  class CancelPendingSwitch {
    constructor() {
      $(document).on("click", ".arraysubs-cancel-pending-switch", async (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        const url = $btn.data("cancel-url");
        if (!url) return;

        const confirmMessage =
          window.arraySubsSubscription?.i18n?.cancelPendingSwitchConfirm ||
          "Cancel the pending plan switch? Your current plan will continue at the next renewal.";

        const ok = await confirmDialog({
          title:
            window.arraySubsSubscription?.i18n
              ?.cancelPendingSwitchTitle ||
            "Cancel Pending Plan Switch",
          message: confirmMessage,
          confirmText:
            window.arraySubsSubscription?.i18n
              ?.cancelPendingSwitchAction ||
            "Cancel Switch",
          cancelText:
            window.arraySubsSubscription?.i18n?.keepSwitch || "Keep Switch",
          variant: "warning",
        });
        if (!ok) return;

        setButtonLoading($btn, true, i18n?.processing || "Processing…");

        $.ajax({
          url,
          method: "POST",
          beforeSend: (xhr) => xhr.setRequestHeader("X-WP-Nonce", nonce),
          success: (response) => {
            portalToast(
              response?.message || "Pending plan switch cancelled.",
              "success",
            );
            setTimeout(() => window.location.reload(), 600);
          },
          error: (xhr) => {
            portalToast(
              xhr.responseJSON?.message ||
                "Failed to cancel the pending plan switch. Please try again.",
              "error",
            );
            setButtonLoading($btn, false);
          },
        });
      });
    }
  }

  /**
   * Undo Scheduled Cancellation Handler
   *
   * Removes a scheduled end-of-period cancellation, reverting the
   * subscription to its normal renewing state. Distinct from
   * ReactivateSubscription, which targets already-cancelled or expired
   * subscriptions.
   */
  class UndoCancellation {
    constructor() {
      this.init();
    }

    init() {
      $(document).on("click", ".arraysubs-undo-cancellation", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.confirmUndo($btn);
      });
    }

    async confirmUndo($btn) {
      const confirmMessage =
        window.arraySubsSubscription?.i18n?.undoCancelConfirm ||
        "Remove the scheduled cancellation? Your subscription will continue renewing normally.";

      const undoUrl = $btn.data("undo-url");
      if (!undoUrl) {
        return;
      }

      await confirmDialog({
        title:
          window.arraySubsSubscription?.i18n?.undoCancelTitle ||
          "Undo Scheduled Cancellation",
        message: confirmMessage,
        confirmText:
          window.arraySubsSubscription?.i18n?.undoCancelAction ||
          "Undo Cancellation",
        variant: "info",
        loadingText: i18n?.processing || "Processing…",
        onConfirm: () =>
          new Promise((resolve, reject) => {
            $.ajax({
              url: undoUrl,
              method: "POST",
              beforeSend: (xhr) => {
                xhr.setRequestHeader("X-WP-Nonce", nonce);
              },
              success: (response) => {
                const message =
                  response?.message ||
                  window.arraySubsSubscription?.i18n?.undoCancelSuccess ||
                  "Scheduled cancellation removed.";
                portalToast(message, "success");
                setTimeout(() => window.location.reload(), 600);
                resolve(response);
              },
              error: (xhr) => {
                const message =
                  xhr.responseJSON?.message ||
                  window.arraySubsSubscription?.i18n?.undoCancelError ||
                  "Failed to remove scheduled cancellation. Please try again.";
                reject(new Error(message));
              },
            });
          }),
      });
    }
  }

  /**
   * Retry Payment Handler
   *
   * Triggers the manual renewal retry endpoint. The backend first verifies
   * with the gateway whether the customer was already charged via a missed
   * webhook before attempting a new charge, so this is safe to click even
   * if the customer is uncertain whether the previous attempt actually went
   * through.
   */
  class RetryPayment {
    constructor() {
      this.init();
    }

    init() {
      $(document).on("click", ".arraysubs-retry-payment", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.confirmRetry($btn);
      });
    }

    async confirmRetry($btn) {
      const confirmMessage =
        window.arraySubsSubscription?.i18n?.retryPaymentConfirm ||
        "Retry the failed renewal payment now? We will first check with the payment provider to make sure you have not already been charged.";

      const ok = await confirmDialog({
        title:
          window.arraySubsSubscription?.i18n?.retryPaymentTitle ||
          "Retry Renewal Payment",
        message: confirmMessage,
        confirmText:
          window.arraySubsSubscription?.i18n?.retryPaymentAction ||
          "Retry Payment",
        variant: "warning",
      });
      if (!ok) return;

      const retryUrl = $btn.data("retry-url");
      if (!retryUrl) {
        return;
      }

      setButtonLoading($btn, true, i18n?.processing || "Processing…");

      $.ajax({
        url: retryUrl,
        method: "POST",
        beforeSend: (xhr) => {
          xhr.setRequestHeader("X-WP-Nonce", nonce);
        },
        success: (response) => {
          const message =
            response?.message ||
            window.arraySubsSubscription?.i18n?.retryPaymentSuccess ||
            "Payment retried successfully.";
          portalToast(message, "success");
          setTimeout(() => window.location.reload(), 600);
        },
        error: (xhr) => {
          const message =
            xhr.responseJSON?.message ||
            window.arraySubsSubscription?.i18n?.retryPaymentError ||
            "Failed to retry payment. Please try again or update your payment method.";
          portalToast(message, "error");
          setButtonLoading($btn, false);
        },
      });
    }
  }

  /**
   * Pause Subscription Handler
   */
  class PauseSubscription {
    constructor() {
      this.init();
    }

    init() {
      // Pause button
      $(document).on("click", ".arraysubs-pause-subscription", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.promptPause($btn);
      });

      // Resume button
      $(document).on("click", ".arraysubs-resume-subscription", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.resume($btn);
      });
    }

    async promptPause($btn) {
      const maxDays = parseInt($btn.data("max-days")) || 90;
      const requireReason = $btn.data("require-reason") === "true";

      let durationDays = 30;
      if (maxDays > 0) {
        const value = await promptDialog({
          title: i18n?.pause_duration_title || "Pause Subscription",
          label:
            i18n?.pause_duration_label ||
            "How many days would you like to pause?",
          message:
            i18n?.pause_duration_help ||
            `Enter a number between 0 and ${maxDays} (0 = indefinite, until manually resumed).`,
          inputType: "number",
          min: 0,
          max: maxDays,
          defaultValue: "30",
          confirmText: i18n?.pause_duration_action || "Continue",
          validate: (val) => {
            const n = parseInt(val, 10);
            if (isNaN(n) || n < 0 || (maxDays > 0 && n > maxDays)) {
              return (
                i18n?.pause_invalid_days ||
                `Please enter a number between 0 and ${maxDays}.`
              );
            }
            return null;
          },
        });
        if (value === null) return;
        durationDays = parseInt(value, 10);
      }

      let reason = "";
      if (requireReason) {
        const value = await promptDialog({
          title: i18n?.pause_reason_title || "Reason for Pausing",
          label:
            i18n?.pause_reason_label ||
            "Please provide a reason for pausing:",
          inputType: "text",
          confirmText: i18n?.pause_reason_action || "Continue",
          validate: (val) => {
            if (!val) {
              return (
                i18n?.pause_reason_required ||
                "A reason is required to pause your subscription."
              );
            }
            return null;
          },
        });
        if (value === null) return;
        reason = value;
      }

      await confirmDialog({
        title: i18n?.pause_confirm_title || "Pause Subscription",
        message:
          i18n?.pause_confirm ||
          "Pause your subscription? You can resume at any time.",
        confirmText: i18n?.pause_confirm_action || "Pause",
        variant: "info",
        loadingText: i18n?.processing || "Processing…",
        onConfirm: () => this.processPause($btn, durationDays, reason),
      });
    }

    processPause($btn, durationDays, reason) {
      const pauseUrl = $btn.data("pause-url");

      return requestPortalJson(pauseUrl, {
        method: "POST",
        body: JSON.stringify({
          days: durationDays,
          reason: reason,
        }),
      })
        .then((response) => {
          const message =
            response?.message ||
            i18n?.pause_success ||
            "Subscription paused successfully!";
          portalToast(message, "success");
          setTimeout(() => window.location.reload(), 600);
          return response;
        })
        .catch((error) => {
          throw new Error(
            error?.payload?.message ||
              error?.message ||
              i18n?.pause_error ||
              "Failed to pause subscription. Please try again.",
          );
        });
    }

    async resume($btn) {
      await confirmDialog({
        title: i18n?.resume_confirm_title || "Resume Subscription",
        message: i18n?.resume_confirm || "Resume your subscription now?",
        confirmText: i18n?.resume_confirm_action || "Resume",
        variant: "info",
        loadingText: i18n?.processing || "Processing…",
        onConfirm: () => this.processResume($btn),
      });
    }

    processResume($btn) {
      const resumeUrl = $btn.data("resume-url");

      return new Promise((resolve, reject) => {
        $.ajax({
          url: resumeUrl,
          method: "POST",
          beforeSend: (xhr) => {
            xhr.setRequestHeader("X-WP-Nonce", nonce);
          },
          success: (response) => {
            const message =
              response?.message ||
              i18n?.resume_success ||
              "Subscription resumed successfully!";
            portalToast(message, "success");
            setTimeout(() => window.location.reload(), 600);
            resolve(response);
          },
          error: (xhr) => {
            const message =
              xhr.responseJSON?.message ||
              i18n?.resume_error ||
              "Failed to resume subscription. Please try again.";
            reject(new Error(message));
          },
        });
      });
    }
  }

  /**
   * Shipping Address Modal Handler
   */
  class ShippingAddressModal {
    constructor() {
      this.modal = $("#arraysubs-shipping-modal");
      this.form = $("#arraysubs-shipping-form");
      this.confirmBtn = this.modal.find(".arraysubs-shipping-save-btn");
      this.cancelBtn = this.modal.find(".arraysubs-modal__cancel-btn");
      this.closeBtn = this.modal.find(".arraysubs-modal__close");
      this.backdrop = this.modal.find(".arraysubs-modal__backdrop");

      this.updateUrl = "";
      this.triggerButton = null;

      if (this.modal.length) {
        this.init();
      }
    }

    init() {
      // Open modal when edit shipping address button clicked
      $(document).on("click", ".arraysubs-edit-shipping-address", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        this.updateUrl = $btn.data("update-url");
        this.triggerButton = $btn;
        this.open();
      });

      // Close modal events
      this.closeBtn.on("click", () => this.close());
      this.cancelBtn.on("click", () => this.close());
      this.backdrop.on("click", () => this.close());

      // Close on ESC key
      $(document).on("keydown", (e) => {
        if (e.key === "Escape" && this.modal.is(":visible")) {
          this.close();
        }
      });

      // Save address
      this.confirmBtn.on("click", () => this.save());
    }

    open() {
      this.confirmBtn
        .prop("disabled", false)
        .text(i18n?.save_address || "Save Address");
      this.modal.fadeIn(200);
      this.modal.find("input:first").focus();
      $("body").addClass("arraysubs-modal-open");
    }

    close() {
      this.modal.fadeOut(200);
      $("body").removeClass("arraysubs-modal-open");
      if (this.triggerButton) {
        this.triggerButton.focus();
      }
    }

    getFormData() {
      const data = {};
      this.form.find("input").each(function () {
        const name = $(this).attr("name");
        if (name) {
          data[name] = $(this).val().trim();
        }
      });
      return data;
    }

    validate(data) {
      const required = [
        "first_name",
        "last_name",
        "address_1",
        "city",
        "postcode",
        "country",
      ];
      for (const field of required) {
        if (!data[field]) {
          const label = this.form
            .find(`[name="${field}"]`)
            .closest(".arraysubs-shipping-form__field")
            .find("label")
            .text()
            .replace(" *", "")
            .trim();
          portalToast(
            (i18n?.field_required || "%s is required.").replace("%s", label),
            "error",
          );
          this.form.find(`[name="${field}"]`).focus();
          return false;
        }
      }
      return true;
    }

    save() {
      const data = this.getFormData();

      if (!this.validate(data)) {
        return;
      }

      setButtonLoading(this.confirmBtn, true, i18n?.saving || "Saving…");

      $.ajax({
        url: this.updateUrl,
        method: "POST",
        contentType: "application/json",
        beforeSend: (xhr) => {
          xhr.setRequestHeader("X-WP-Nonce", nonce);
        },
        data: JSON.stringify({
          shipping_address: data,
        }),
        success: (response) => {
          const message =
            response?.message ||
            i18n?.address_updated ||
            "Shipping address updated successfully!";
          portalToast(message, "success");
          setTimeout(() => window.location.reload(), 600);
        },
        error: (xhr) => {
          const message =
            xhr.responseJSON?.message ||
            i18n?.address_update_error ||
            "Failed to update shipping address. Please try again.";
          portalToast(message, "error");
          setButtonLoading(this.confirmBtn, false);
        },
      });
    }
  }

  /**
   * Payment Method Update Handler
   *
   * Handles "Update payment method" link clicks rendered by the gateway
   * system. Calls the REST API to get a redirect URL (e.g., Stripe
   * Customer Portal) and navigates the customer there.
   */
  function initPaymentMethodUpdate() {
    $(document).on(
      "click",
      ".arraysubs-update-payment-method-link",
      async function (e) {
        e.preventDefault();

        const $link = $(this);
        const subscriptionId = $link.data("subscription-id");
        const updateUrl =
          $link.data("update-url") ||
          buildApiUrl(`subscriptions/${subscriptionId}/payment-method/update`);

        if (!subscriptionId || !updateUrl) {
          return;
        }

        setButtonLoading(
          $link,
          true,
          i18n?.updating_payment_method || "Updating…",
        );
        $link.css("pointer-events", "none");

        const returnUrl = window.location.href;

        try {
          const response = await requestPortalJson(updateUrl, {
            body: JSON.stringify({ return_url: returnUrl }),
          });

          if (response.success && response.data) {
            const data = response.data;

            // Handle redirect-based update (e.g., Stripe Customer Portal)
            if (data.redirect_url) {
              window.location.href = data.redirect_url;
              return;
            }

            // Handle hosted page update (e.g., Stripe Checkout in setup mode)
            if (data.url) {
              window.location.href = data.url;
              return;
            }
          }

          setButtonLoading($link, false);
          $link.css("pointer-events", "");
          portalToast(
            response.message ||
              i18n?.payment_method_update_error ||
              "Unable to update payment method.",
            "error",
          );
        } catch (error) {
          setButtonLoading($link, false);
          $link.css("pointer-events", "");
          const message =
            error?.payload?.message ||
            i18n?.payment_method_update_error ||
            "Unable to update payment method. Please try again.";
          portalToast(message, "error");
        }
      },
    );
  }

  // Initialize on DOM ready
  $(function () {
    initSubscriptionsMenuBadge();

    const cancelModal = new CancelModal();
    new PlanSwitchModal();
    new SkipRenewal();
    new PauseSubscription();
    new ReactivateSubscription();
    new UndoCancellation();
    new CancelPendingSwitch();
    new RetryPayment();
    new ShippingAddressModal();
    initPaymentMethodUpdate();

    // Initialize retention offers if enabled
    const retentionOffers = new RetentionOffers();

    // Override cancel button to show retention offers first.
    // The retention modal HTML is only rendered by PHP when retention is enabled
    // AND the customer can cancel, so its presence in the DOM is the reliable check.
    if (retentionOffers.modal.length) {
      // Track whether user has seen retention offers
      let hasSeenRetentionOffers = false;

      $(document).off("click", ".arraysubs-cancel-subscription");
      $(document).on("click", ".arraysubs-cancel-subscription", (e) => {
        e.preventDefault();
        const $btn = $(e.currentTarget);
        const subscriptionId = $btn.data("subscription-id");

        // Reset state for new cancellation flow
        hasSeenRetentionOffers = false;

        // Store cancel URL for later
        cancelModal.cancelUrl = $btn.data("cancel-url");
        cancelModal.subscriptionId = subscriptionId;
        cancelModal.triggerButton = $btn;

        // Check if we need to ask for reason first
        const requireReason =
          window.arraySubsSubscription?.requireReason ||
          cancelModal.reasonSelect.prop("required");

        // Show the cancel modal first to get the reason, then show retention offers
        if (requireReason || cancelModal.reasonSelect.length) {
          // Show cancel modal to collect reason first
          cancelModal.open();

          // Override the confirm button for reason collection phase
          if (!cancelModal.confirmBtn.data("retentionBound")) {
            cancelModal.confirmBtn.data("retentionBound", true);

            // Remove the original click handler
            cancelModal.confirmBtn.off("click");

            // Add our custom handler
            cancelModal.confirmBtn.on("click", function () {
              const reason = cancelModal.reasonSelect.val();
              const otherReason = cancelModal.otherReasonTextarea.val();

              // Validate reason if required — inline error inside the modal.
              if (requireReason && !reason) {
                const reasonRequiredMsg =
                  window.arraySubsSubscription?.i18n?.reasonRequired ||
                  "Please select a reason for cancelling.";
                let $err = cancelModal.modal.find(
                  ".arraysubs-modal__inline-error",
                );
                if (!$err.length) {
                  $err = $(
                    '<p class="arraysubs-modal__inline-error" role="alert" style="margin:8px 0 0;color:#b91c1c;font-size:13px;"></p>',
                  );
                  cancelModal.reasonSelect
                    .closest(".arraysubs-modal__form")
                    .append($err);
                }
                $err.text(reasonRequiredMsg).show();
                cancelModal.reasonSelect.trigger("focus");
                return;
              }

              // If user has already seen retention offers, proceed with cancellation
              if (hasSeenRetentionOffers) {
                cancelModal.confirm();
                return;
              }

              // Close the cancel modal
              cancelModal.close();

              // Mark that user will see retention offers
              hasSeenRetentionOffers = true;

              // Show retention offers with the selected reason
              setTimeout(() => {
                retentionOffers.show(
                  subscriptionId,
                  cancelModal,
                  reason,
                  otherReason,
                );
              }, 250);
            });
          }
        } else {
          // No reason selection needed, show retention offers directly
          hasSeenRetentionOffers = true;
          retentionOffers.show(subscriptionId, cancelModal);
        }
      });
    }
  });
})(jQuery);
