// Shared inline notice — same look used after buying a phone number.
// Usage: StoreVoiceAINotice.show({ target: $el, type: 'success'|'error', message, position: 'before'|'after' })
window.StoreVoiceAINotice = (function ($) {
  const noticeStore = new WeakMap();

  function ensureNotice($target, position) {
    let entry = noticeStore.get($target.get(0));
    if (entry && entry.$notice && entry.$notice.closest("body").length) {
      return entry;
    }

    const $notice = $(
      '<div class="storevoiceai-notice" role="status" aria-live="polite" hidden>' +
        '<span class="dashicons" aria-hidden="true"></span>' +
        '<span class="storevoiceai-notice__message"></span>' +
        '<button type="button" class="button storevoiceai-notice__action" hidden></button>' +
        '<button type="button" class="storevoiceai-notice__close" aria-label="Dismiss notice">&times;</button>' +
      '</div>',
    );

    if (position === "after") {
      $target.after($notice);
    } else {
      $target.before($notice);
    }

    $notice.on("click", ".storevoiceai-notice__close", function () {
      hide($notice);
    });

    entry = { $notice: $notice, timer: null };
    noticeStore.set($target.get(0), entry);
    return entry;
  }

  function hide($notice) {
    $notice
      .removeClass(
        "storevoiceai-notice--is-visible storevoiceai-notice--success storevoiceai-notice--error",
      )
      .attr("hidden", true);
  }

  function show(options) {
    const opts = options || {};
    const $target = opts.target;
    if (!$target || !$target.length) return;

    const type = opts.type === "error" ? "error" : "success";
    const message = opts.message || "";
    const entry = ensureNotice($target, opts.position || "before");
    const $notice = entry.$notice;

    if (entry.timer) {
      clearTimeout(entry.timer);
      entry.timer = null;
    }

    const iconClass =
      type === "success" ? "dashicons-yes-alt" : "dashicons-warning";

    $notice
      .removeClass(
        "storevoiceai-notice--success storevoiceai-notice--error",
      )
      .addClass("storevoiceai-notice--" + type)
      .removeAttr("hidden");

    $notice
      .find(".dashicons")
      .attr("class", "dashicons " + iconClass);
    $notice.find(".storevoiceai-notice__message").text(message);

    const $action = $notice.find(".storevoiceai-notice__action");
    $action.off("click.storevoiceaiNotice");
    if (opts.action && opts.action.label) {
      $action.text(opts.action.label).removeAttr("hidden");
      if (typeof opts.action.onClick === "function") {
        $action.on("click.storevoiceaiNotice", function () {
          opts.action.onClick($notice);
        });
      }
    } else {
      $action.attr("hidden", true).text("");
    }

    requestAnimationFrame(function () {
      $notice.addClass("storevoiceai-notice--is-visible");
    });

    if (opts.autoDismiss !== false) {
      entry.timer = setTimeout(function () {
        hide($notice);
      }, opts.duration || 5000);
    }
  }

  return { show: show, hide: hide };
})(jQuery);

function escapeHtml(s) {
  return String(s ?? "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

jQuery(document).ready(function ($) {
  const getHumanReadableDateTime = (isoDate) => {
    return new Date(isoDate).toLocaleString("en-US", {
      year: "numeric",
      month: "long",
      day: "numeric",
      hour: "numeric",
      minute: "numeric",
      second: "numeric",
    });
  };

  const formatDuration = (milliseconds) => {
    // Calculate total seconds
    const totalSeconds = Math.floor(parseInt(milliseconds, 10) / 1000);

    // Calculate hours, minutes, and seconds
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;

    // Format hours, minutes, and seconds as two-digit numbers
    const formattedHours = String(hours).padStart(2, "0");
    const formattedMinutes = String(minutes).padStart(2, "0");
    const formattedSeconds = String(seconds).padStart(2, "0");

    // Construct and return the formatted time string
    return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
  };

  const getAmountInDollars = (amountInCents) => {
    return (parseFloat(amountInCents) / 100).toLocaleString(undefined, {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    });
  };

  async function initializeVoices() {
    const voiceDropdown = document.getElementById("voiceDropdown");
    if (!voiceDropdown) return;

    const genderFilter = document.getElementById("voiceGenderFilter");
    const accentFilter = document.getElementById("voiceAccentFilter");
    const voiceAvatar = document.querySelector(".voice-avatar__initial");
    const voicePreviewButton = document.getElementById("storevoiceai-voice-preview-button");
    const voicePreviewAudio = document.getElementById("storevoiceai-voice-preview-audio");
    const voiceDropdownButton = document.getElementById("voiceDropdownButton");
    const voiceDropdownLabel = document.getElementById("voiceDropdownLabel");
    const voiceDropdownMenu = document.getElementById("voiceDropdownMenu");

    let allVoices = [];
    let voicesById = {};
    let activePreviewButton = null;
    let selectedVoiceId =
      $("#storevoiceai-create-assistant__button").data("saved-voice") || "";

    const titleCase = (s) =>
      String(s || "").replace(/\b\w/g, (c) => c.toUpperCase());

    const formatVoiceLabel = (voice) => {
      const parts = [voice.voice_name];
      const meta = [];
      if (voice.gender) meta.push(titleCase(voice.gender));
      if (voice.accent) meta.push(titleCase(voice.accent));
      if (meta.length) parts.push(`(${meta.join(", ")})`);
      return parts.join(" ");
    };

    const getVoicePreviewUrl = (voice) => {
      const rawUrl = String((voice && voice.preview_audio_url) || "").trim();
      if (!rawUrl) return "";

      try {
        const url = new URL(rawUrl, window.location.href);
        return url.protocol === "http:" || url.protocol === "https:" ? url.href : "";
      } catch (e) {
        return "";
      }
    };

    const setPreviewButtonState = (button, isPlaying) => {
      if (!button) return;

      const icon = button.querySelector(".dashicons");
      const label = button.querySelector(".voice-preview__button-label, .voice-option__play-label");
      const voiceName = button.getAttribute("data-voice-name") || "voice";
      const playLabel = `Play ${voiceName} demo`;
      const pauseLabel = `Pause ${voiceName} demo`;

      button.classList.toggle("is-playing", isPlaying);
      button.setAttribute("aria-label", isPlaying ? pauseLabel : playLabel);
      button.setAttribute("title", isPlaying ? pauseLabel : playLabel);

      if (icon) {
        icon.classList.toggle("dashicons-controls-play", !isPlaying);
        icon.classList.toggle("dashicons-controls-pause", isPlaying);
      }
      if (label) {
        label.textContent = isPlaying ? pauseLabel : playLabel;
      }
    };

    const markPreviewButtonError = (button) => {
      if (!button) return;
      button.disabled = false;
      button.classList.add("voice-preview__button--error");
      button.setAttribute("title", "This voice demo could not be played.");
    };

    const playVoicePreview = (voiceId, button) => {
      const voice = voicesById[voiceId];
      const previewUrl = getVoicePreviewUrl(voice);

      if (!voice || !previewUrl || !voicePreviewAudio || !button || button.disabled) return;

      if (
        activePreviewButton === button &&
        voicePreviewAudio.src === previewUrl &&
        !voicePreviewAudio.paused
      ) {
        voicePreviewAudio.pause();
        return;
      }

      if (!voicePreviewAudio.paused) {
        voicePreviewAudio.pause();
      }
      if (activePreviewButton && activePreviewButton !== button) {
        setPreviewButtonState(activePreviewButton, false);
      }

      activePreviewButton = button;
      button.classList.remove("voice-preview__button--error");
      button.disabled = true;
      const label = button.querySelector(".voice-preview__button-label, .voice-option__play-label");
      if (label) label.textContent = "Loading voice demo";

      voicePreviewAudio.src = previewUrl;
      const playPromise = voicePreviewAudio.play();
      if (playPromise && typeof playPromise.then === "function") {
        playPromise
          .then(function () {
            button.disabled = false;
          })
          .catch(function () {
            markPreviewButtonError(button);
            setPreviewButtonState(button, false);
          });
      } else {
        button.disabled = false;
      }
    };

    const updateVoicePreview = (voiceId) => {
      const voice = voicesById[voiceId];
      const previewUrl = getVoicePreviewUrl(voice);

      if (voicePreviewButton) {
        voicePreviewButton.classList.remove(
          "voice-preview__button--unavailable",
          "voice-preview__button--error",
        );
      }

      if (!voice) {
        if (voicePreviewButton) voicePreviewButton.disabled = true;
        return;
      }

      if (voicePreviewButton) {
        voicePreviewButton.setAttribute("data-voice-name", voice.voice_name || "voice");
      }

      if (!previewUrl || !voicePreviewAudio || !voicePreviewButton) {
        if (voicePreviewButton) voicePreviewButton.classList.add("voice-preview__button--unavailable");
        if (voicePreviewButton) {
          voicePreviewButton.disabled = true;
          voicePreviewButton.setAttribute("aria-label", "No voice demo available");
          voicePreviewButton.setAttribute("title", "No voice demo available");
        }
        return;
      }

      voicePreviewButton.disabled = false;
      setPreviewButtonState(voicePreviewButton, false);
    };

    const updateVoiceAvatar = (voiceId) => {
      const voice = voicesById[voiceId];
      if (voice && voiceAvatar) {
        voiceAvatar.textContent = (voice.voice_name || "").charAt(0).toUpperCase();
      }
    };

    const populateFilterOptions = (select, values) => {
      if (!select) return;
      const placeholder = select.querySelector("option[value='']");
      select.innerHTML = "";
      if (placeholder) select.appendChild(placeholder);
      values.forEach((v) => {
        const opt = document.createElement("option");
        opt.value = v;
        opt.textContent = titleCase(v);
        select.appendChild(opt);
      });
    };

    const closeVoiceMenu = () => {
      if (!voiceDropdownButton || !voiceDropdownMenu) return;
      voiceDropdownButton.setAttribute("aria-expanded", "false");
      voiceDropdownMenu.setAttribute("hidden", true);
    };

    const openVoiceMenu = () => {
      if (!voiceDropdownButton || !voiceDropdownMenu) return;
      voiceDropdownButton.setAttribute("aria-expanded", "true");
      voiceDropdownMenu.removeAttribute("hidden");
    };

    const syncVoiceSelection = (voiceId, closeAfterSelect) => {
      if (!voiceId || !voicesById[voiceId]) return;

      selectedVoiceId = voiceId;
      voiceDropdown.value = voiceId;
      if (voiceDropdownLabel) {
        voiceDropdownLabel.textContent = formatVoiceLabel(voicesById[voiceId]);
      }

      updateVoiceAvatar(voiceId);
      updateVoicePreview(voiceId);

      if (voiceDropdownMenu) {
        voiceDropdownMenu.querySelectorAll(".voice-dropdown__option").forEach((option) => {
          const isSelected = option.getAttribute("data-voice-id") === voiceId;
          option.classList.toggle("is-selected", isSelected);
          option.setAttribute("aria-selected", isSelected ? "true" : "false");
        });
      }

      if (closeAfterSelect) {
        closeVoiceMenu();
        if (voiceDropdownButton) voiceDropdownButton.focus();
      }
    };

    const renderCustomVoiceMenu = (voices) => {
      if (!voiceDropdownMenu) return;

      voiceDropdownMenu.innerHTML = "";
      if (!voices.length) {
        const empty = document.createElement("div");
        empty.className = "voice-dropdown__empty";
        empty.textContent = "No voices match these filters";
        voiceDropdownMenu.appendChild(empty);
        return;
      }

      voices.forEach((voice) => {
        const item = document.createElement("div");
        const previewUrl = getVoicePreviewUrl(voice);
        const playButton = document.createElement("button");
        const label = document.createElement("span");
        const meta = [];

        item.className = "voice-dropdown__option";
        item.setAttribute("role", "option");
        item.setAttribute("tabindex", "-1");
        item.setAttribute("data-voice-id", voice.voice_id);
        item.setAttribute("aria-selected", voice.voice_id === selectedVoiceId ? "true" : "false");
        item.classList.toggle("is-selected", voice.voice_id === selectedVoiceId);

        playButton.type = "button";
        playButton.className = "voice-option__play";
        playButton.setAttribute("data-voice-name", voice.voice_name || "voice");
        playButton.innerHTML =
          '<span class="dashicons dashicons-controls-play" aria-hidden="true"></span>' +
          '<span class="screen-reader-text voice-option__play-label"></span>';
        setPreviewButtonState(playButton, false);
        if (!previewUrl) {
          playButton.disabled = true;
          playButton.classList.add("voice-preview__button--unavailable");
          playButton.setAttribute("aria-label", "No voice demo available");
          playButton.setAttribute("title", "No voice demo available");
        }
        playButton.addEventListener("click", function (e) {
          e.preventDefault();
          e.stopPropagation();
          playVoicePreview(voice.voice_id, playButton);
        });

        label.className = "voice-dropdown__option-text";
        label.textContent = voice.voice_name || voice.voice_id;
        if (voice.gender) meta.push(titleCase(voice.gender));
        if (voice.accent) meta.push(titleCase(voice.accent));
        if (meta.length) {
          const metaText = document.createElement("span");
          metaText.className = "voice-dropdown__option-meta";
          metaText.textContent = meta.join(", ");
          label.appendChild(metaText);
        }

        item.appendChild(playButton);
        item.appendChild(label);
        item.addEventListener("click", function () {
          syncVoiceSelection(voice.voice_id, true);
        });
        voiceDropdownMenu.appendChild(item);
      });
    };

    const renderVoiceOptions = () => {
      const gender = (genderFilter && genderFilter.value) || "";
      const accent = (accentFilter && accentFilter.value) || "";

      const filtered = allVoices.filter((v) => {
        if (gender && String(v.gender || "").toLowerCase() !== gender) return false;
        if (accent && String(v.accent || "").toLowerCase() !== accent) return false;
        return true;
      });

      voiceDropdown.innerHTML = "";

      if (!filtered.length) {
        const opt = document.createElement("option");
        opt.value = "";
        opt.textContent = "No voices match these filters";
        opt.disabled = true;
        opt.selected = true;
        voiceDropdown.appendChild(opt);
        if (voiceAvatar) voiceAvatar.textContent = "–";
        updateVoicePreview("");
        if (voiceDropdownLabel) voiceDropdownLabel.textContent = "No voices match these filters";
        renderCustomVoiceMenu([]);
        return;
      }

      let selectedFound = false;
      filtered.forEach((voice) => {
        const option = document.createElement("option");
        option.value = voice.voice_id;
        option.textContent = formatVoiceLabel(voice);
        if (selectedVoiceId && voice.voice_id === selectedVoiceId) {
          option.selected = true;
          selectedFound = true;
        }
        voiceDropdown.appendChild(option);
      });

      if (!selectedFound) {
        voiceDropdown.selectedIndex = 0;
      }
      selectedVoiceId = voiceDropdown.value;
      renderCustomVoiceMenu(filtered);
      syncVoiceSelection(voiceDropdown.value, false);
    };

    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: { action: "storevoiceai_get_voices", nonce: storevoiceaiCallLogger.nonce },
      success: function (response) {
        if (!response.success) return;
        const voiceList = Array.isArray(response.data)
          ? response.data
          : response.data.data || [];
        if (!voiceList.length) {
          console.warn("No voices returned from API.");
          return;
        }

        allVoices = voiceList;
        voicesById = {};
        voiceList.forEach((v) => {
          voicesById[v.voice_id] = v;
        });

        const genders = Array.from(
          new Set(voiceList.map((v) => String(v.gender || "").toLowerCase()).filter(Boolean))
        ).sort();
        const accents = Array.from(
          new Set(voiceList.map((v) => String(v.accent || "").toLowerCase()).filter(Boolean))
        ).sort();
        populateFilterOptions(genderFilter, genders);
        populateFilterOptions(accentFilter, accents);

        renderVoiceOptions();
      },
      error: function () {
        console.error("Failed to load voices.");
      },
    });

    if (genderFilter) genderFilter.addEventListener("change", renderVoiceOptions);
    if (accentFilter) accentFilter.addEventListener("change", renderVoiceOptions);

    voiceDropdown.addEventListener("change", function (e) {
      const voiceId = e.target.value;
      if (!voiceId) return;
      syncVoiceSelection(voiceId, false);
    });

    if (voicePreviewButton && voicePreviewAudio) {
      voicePreviewButton.addEventListener("click", function () {
        playVoicePreview(selectedVoiceId, voicePreviewButton);
      });

      voicePreviewAudio.addEventListener("play", function () {
        if (activePreviewButton) {
          activePreviewButton.disabled = false;
          activePreviewButton.classList.remove("voice-preview__button--error");
          setPreviewButtonState(activePreviewButton, true);
        }
      });

      voicePreviewAudio.addEventListener("pause", function () {
        if (activePreviewButton) {
          setPreviewButtonState(activePreviewButton, false);
        }
      });

      voicePreviewAudio.addEventListener("ended", function () {
        voicePreviewAudio.currentTime = 0;
        if (activePreviewButton) {
          setPreviewButtonState(activePreviewButton, false);
          activePreviewButton = null;
        }
      });

      voicePreviewAudio.addEventListener("error", function () {
        markPreviewButtonError(activePreviewButton);
        if (activePreviewButton) {
          setPreviewButtonState(activePreviewButton, false);
        }
      });
    }

    if (voiceDropdownButton && voiceDropdownMenu) {
      voiceDropdownButton.addEventListener("click", function () {
        if (voiceDropdownMenu.hasAttribute("hidden")) {
          openVoiceMenu();
        } else {
          closeVoiceMenu();
        }
      });

      voiceDropdownButton.addEventListener("keydown", function (e) {
        if (e.key !== "ArrowDown" && e.key !== "Enter" && e.key !== " ") return;
        e.preventDefault();
        openVoiceMenu();
        const selectedOption =
          voiceDropdownMenu.querySelector(".voice-dropdown__option.is-selected") ||
          voiceDropdownMenu.querySelector(".voice-dropdown__option");
        if (selectedOption) selectedOption.focus();
      });

      voiceDropdownMenu.addEventListener("keydown", function (e) {
        const options = Array.from(voiceDropdownMenu.querySelectorAll(".voice-dropdown__option"));
        const index = options.indexOf(document.activeElement);

        if (e.key === "Escape") {
          e.preventDefault();
          closeVoiceMenu();
          voiceDropdownButton.focus();
          return;
        }
        if (e.key === "ArrowDown" || e.key === "ArrowUp") {
          e.preventDefault();
          const nextIndex =
            e.key === "ArrowDown"
              ? Math.min(index + 1, options.length - 1)
              : Math.max(index - 1, 0);
          if (options[nextIndex]) options[nextIndex].focus();
          return;
        }
        if ((e.key === "Enter" || e.key === " ") && document.activeElement) {
          e.preventDefault();
          document.activeElement.click();
        }
      });

      document.addEventListener("click", function (e) {
        if (!voiceDropdownButton.contains(e.target) && !voiceDropdownMenu.contains(e.target)) {
          closeVoiceMenu();
        }
      });
    }
  }

  initializeVoices();

  // Load call logs if on call logs page
  if ($("#call-logs-table").length) {
    loadCallLogs();
  }

  $(document).on("click", "#storevoiceai-sync-calls", function (e) {
    e.preventDefault();
    syncCalls($(this));
  });

  // Load dashboard data if on dashboard page
  if ($("#call-summary-dashboard").length) {
    loadDashboardData(getActiveRangeDays());

    $(document).on("click", ".dashboard-range-tab", function () {
      const $tab = $(this);
      if ($tab.hasClass("is-active")) return;
      $(".dashboard-range-tab").removeClass("is-active").attr("aria-selected", "false");
      $tab.addClass("is-active").attr("aria-selected", "true");
      loadDashboardData(parseInt($tab.attr("data-range-days"), 10) || 30);
    });

    if (window.storevoiceaiTour && window.storevoiceaiTour.enabled) {
      startDashboardTour();
    }
  }

  let stripeEmbeddedCheckout = null;
  let activeTopUpButton = null;
  let stripeCheckoutInitToken = 0;
  let billingConnectionStatus = null;
  let billingConnectionStatusRequest = null;

  // Load minutes quota + daily usage plot if on billing page
  if ($("#billing-and-usage").length) {
    loadBillingConnectionStatus().always(function () {
      loadMinutesQuota();
      loadDailyUsagePlot(getActiveMinutesRangeDays());
      loadWalletTransactions();
    });
    showTopUpProcessingNoticeFromReturnUrl();

    $(document).on("click", ".minutes-range-tab", function () {
      const $tab = $(this);
      if ($tab.hasClass("is-active")) return;
      $(".minutes-range-tab").removeClass("is-active").attr("aria-selected", "false");
      $tab.addClass("is-active").attr("aria-selected", "true");
      loadDailyUsagePlot(parseInt($tab.attr("data-range-days"), 10) || 30);
    });

    $(document).on("click", ".billing-topup-option", function () {
      const $option = $(this);
      if ($option.prop("disabled")) return;

      const $card = $option.closest(".billing-card--wallet");
      const amount = Number($option.attr("data-amount") || 0);

      $card.find(".billing-topup-option").removeClass("is-active").attr("aria-pressed", "false");
      $option.addClass("is-active").attr("aria-pressed", "true");
      $card.find(".storevoiceai-topup-checkout")
        .attr("data-selected-amount", amount)
        .attr("aria-disabled", "false");
    });

    $(document).on("click", ".storevoiceai-topup-checkout", function (e) {
      e.preventDefault();
      const $button = $(this);
      const amount = Number($button.attr("data-selected-amount") || 0);
      if ($button.attr("aria-busy") === "true" || $button.attr("aria-disabled") === "true") return;

      createStripeTopUpCheckout(amount, $button);
    });

    $(document).on("click", ".storevoiceai-stripe-modal__close", function () {
      closeStripeTopUpModal();
    });
  } else if (storevoiceaiCallLogger.has_agent) {
    if (isPricingPage()) {
      loadMinutesUsageState();
    } else {
      loadMinutesUsageAlert();
    }
  }

  function loadBillingConnectionStatus(options) {
    const opts = options || {};
    if (billingConnectionStatusRequest && !opts.refresh) {
      return billingConnectionStatusRequest;
    }

    billingConnectionStatusRequest = $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_get_billing_connection_status",
        nonce: storevoiceaiCallLogger.nonce,
      },
      success: function (response) {
        if (response && response.success && response.data) {
          billingConnectionStatus = normalizeBillingConnectionStatus(response.data);
          return;
        }

        billingConnectionStatus = {
          error: true,
          message: getAjaxMessage(response && response.data, "Could not verify payment setup."),
        };
      },
      error: function (xhr) {
        billingConnectionStatus = {
          error: true,
          message: getAjaxMessage(xhr.responseJSON && xhr.responseJSON.data, "Could not verify payment setup."),
        };
      },
      complete: function () {
        billingConnectionStatusRequest = null;
      },
    });

    return billingConnectionStatusRequest;
  }

  function normalizeBillingConnectionStatus(data) {
    const source = data || {};
    return {
      gateway: String(source.gateway || "").toLowerCase(),
      isConnected: source.isConnected === true,
      isComplete: source.isComplete === true,
      accountId: String(source.accountId || ""),
      needsOnboarding: source.needsOnboarding === true,
      requirements: source.requirements || null,
      error: false,
    };
  }

  function getTopUpGatewayState() {
    const status = billingConnectionStatus;

    if (!status) {
      return {
        mode: "disabled",
        message: "Checking payment setup...",
      };
    }

    if (status.error) {
      return {
        mode: "disabled",
        message: status.message || "Could not verify payment setup.",
      };
    }

    if (status.gateway === "manual") {
      return {
        mode: "hidden",
        message: "",
      };
    }

    if (!status.isConnected || !status.isComplete || status.needsOnboarding) {
      return {
        mode: "disabled",
        message: "The agency hasn't finished payment setup yet.",
      };
    }

    if (status.gateway === "stripe_express") {
      if (!status.accountId) {
        return {
          mode: "disabled",
          message: "Stripe Express account information is missing.",
        };
      }

      return {
        mode: "embedded",
        accountId: status.accountId,
        message: "",
      };
    }

    if (status.gateway === "stripe_standard") {
      return {
        mode: "hosted",
        message: "",
      };
    }

    return {
      mode: "disabled",
      message: "Top-ups are not available for this payment gateway.",
    };
  }

  function createStripeTopUpCheckout(amount, $button) {
    setTopUpButtonLoading($button, true);

    loadBillingConnectionStatus({ refresh: true })
      .done(function () {
        const gatewayState = getTopUpGatewayState();
        if (gatewayState.mode === "hidden" || gatewayState.mode === "disabled") {
          showTopUpCheckoutError($button, { message: gatewayState.message || "Top-ups are not available." });
          setTopUpButtonLoading($button, false);
          return;
        }

        if (!isValidTopUpAmount(amount)) {
          showTopUpCheckoutError($button, { message: "Please choose a valid top-up amount." });
          setTopUpButtonLoading($button, false);
          return;
        }

        if (gatewayState.mode === "embedded") {
          if (!storevoiceaiCallLogger.stripe_publishable_key) {
            showTopUpCheckoutError($button, { message: "Stripe is not configured yet. Add the Stripe publishable key before accepting top-ups." });
            setTopUpButtonLoading($button, false);
            return;
          }

          if (typeof window.Stripe !== "function") {
            showTopUpCheckoutError($button, { message: "Stripe checkout is still loading. Please try again in a moment." });
            setTopUpButtonLoading($button, false);
            return;
          }
        }

        if (gatewayState.mode === "hosted") {
          createStripeHostedTopUpSession(amount, $button, gatewayState);
          return;
        }

        openStripeTopUpModal(amount, $button, gatewayState);
      })
      .fail(function () {
        showTopUpCheckoutError($button, { message: "Could not verify payment setup. Please try again." });
        setTopUpButtonLoading($button, false);
      });
  }

  function requestStripeTopUpSession(amount, gatewayState) {
    return $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_create_stripe_top_up_session",
        nonce: storevoiceaiCallLogger.nonce,
        amount: amount,
        return_url: buildTopUpReturnUrl(amount),
      },
    });
  }

  function extractStripeSessionPayload(response) {
    if (!response) {
      return null;
    }

    const data = response.data || response;
    const nested = data.data && typeof data.data === "object" ? data.data : data;

    return {
      clientSecret: nested.clientSecret || nested.client_secret || "",
      checkoutUrl: nested.checkoutUrl || nested.checkout_url || nested.url || "",
    };
  }

  function describeStripeClientSecret(clientSecret) {
    const secret = String(clientSecret || "");

    return {
      prefix: secret.slice(0, 32),
      suffix: secret.slice(-16),
      length: secret.length,
      hasSecretSegment: secret.indexOf("_secret_") !== -1,
    };
  }

  function createStripeHostedTopUpSession(amount, $button, gatewayState) {
    requestStripeTopUpSession(amount, gatewayState)
      .done(function (response) {
        const payload = extractStripeSessionPayload(response);
        if (!payload) {
          showTopUpCheckoutError($button, response && response.data);
          return;
        }

        if (payload.checkoutUrl) {
          window.location.href = payload.checkoutUrl;
          return;
        }

        showTopUpCheckoutError($button, { message: "Hosted Stripe checkout URL was not returned by the backend." });
      })
      .fail(function (xhr) {
        showTopUpCheckoutError($button, xhr.responseJSON && xhr.responseJSON.data);
      })
      .always(function () {
        setTopUpButtonLoading($button, false);
      });
  }

  function logStripeTopUpDebug(message, data) {
    if (window.console && typeof window.console.info === "function") {
      window.console.info("[StoreVoiceAI topup] " + message, data || {});
    }
  }

  function logStripeTopUpSecret(clientSecret, gatewayState) {
    if (!window.console || typeof window.console.info !== "function") return;

    const description = describeStripeClientSecret(clientSecret);
    window.console.info(
      "[StoreVoiceAI topup] secret being mounted prefix=%s suffix=%s length=%d hasSecretSegment=%s gateway=%s accountId=%s",
      description.prefix,
      description.suffix,
      description.length,
      description.hasSecretSegment ? "yes" : "no",
      gatewayState.mode,
      gatewayState.accountId
    );
  }

  function showTopUpCheckoutError($button, payload) {
    const message = payload && payload.message
      ? payload.message
      : "Stripe checkout could not be started. Please try again.";

    window.StoreVoiceAINotice.show({
      target: $button.closest(".billing-card"),
      type: "error",
      message: message,
      position: "after",
      duration: 10000,
    });
  }

  function getAjaxMessage(payload, fallback) {
    if (payload && payload.message) return payload.message;
    if (typeof payload === "string" && payload) return payload;
    return fallback;
  }

  function setTopUpButtonLoading($button, loading) {
    if (!$button || !$button.length) return;

    $button
      .attr("aria-busy", loading ? "true" : "false")
      .toggleClass("is-disabled", loading)
      .text(loading ? "Opening..." : "Top Up");
  }

  function openStripeTopUpModal(amount, $button, gatewayState) {
    const $modal = ensureStripeTopUpModal();
    const $mount = $("#storevoiceai-stripe-checkout");
    const stripe = window.Stripe(storevoiceaiCallLogger.stripe_publishable_key, {
      stripeAccount: gatewayState.accountId,
    });

    closeStripeTopUpModal({ keepModal: true, keepButton: true });
    const initToken = stripeCheckoutInitToken + 1;
    stripeCheckoutInitToken = initToken;
    activeTopUpButton = $button;
    $modal.find(".storevoiceai-stripe-modal__amount").text(formatMoney(amount));
    $modal.removeAttr("hidden").attr("aria-hidden", "false");
    $mount.empty().addClass("storevoiceai-stripe-checkout--loading").html(
      '<div class="storevoiceai-stripe-checkout__loading">' +
        '<span class="storevoiceai-spinner" aria-hidden="true"></span>' +
        '<span class="storevoiceai-stripe-checkout__loading-text">Loading checkout...</span>' +
      '</div>'
    );

    const timeout = window.setTimeout(function () {
      if (stripeCheckoutInitToken !== initToken || stripeEmbeddedCheckout) return;

      closeStripeTopUpModal();
      window.StoreVoiceAINotice.show({
        target: $button.closest(".billing-card"),
        type: "error",
        message: "Stripe checkout is taking too long to load. Please try again.",
        position: "after",
        duration: 10000,
      });
    }, 15000);

    function failStripeCheckout(error) {
      window.clearTimeout(timeout);
      if (stripeCheckoutInitToken !== initToken) return;

      logStripeTopUpDebug("embedded checkout init failed", {
        message: error && error.message,
        name: error && error.name,
      });

      closeStripeTopUpModal();
      window.StoreVoiceAINotice.show({
        target: $button.closest(".billing-card"),
        type: "error",
        message: "Stripe checkout could not be loaded. Please try again.",
        position: "after",
        duration: 10000,
      });
    }

    // The backend pre-creates the embedded session and returns its secret. Mount
    // it with the static clientSecret form rather than the fetchClientSecret
    // callback: for a connected account with an already-created session, the
    // callback form leaves Stripe waiting on a secret it never receives, which
    // surfaces as "Timed out waiting for client secret" / "Invalid Checkout session".
    requestStripeTopUpSession(amount, gatewayState)
      .done(function (response) {
        if (stripeCheckoutInitToken !== initToken) return;

        const payload = extractStripeSessionPayload(response);
        if (!payload || !payload.clientSecret) {
          logStripeTopUpDebug("session response missing client secret", {
            responseKeys: response && typeof response === "object" ? Object.keys(response) : [],
            dataKeys: response && response.data && typeof response.data === "object" ? Object.keys(response.data) : [],
          });
          failStripeCheckout(new Error(getAjaxMessage(response && response.data, "Stripe checkout did not return a client secret.")));
          return;
        }

        logStripeTopUpSecret(payload.clientSecret, gatewayState);

        stripe.initEmbeddedCheckout({ clientSecret: payload.clientSecret })
          .then(function (checkout) {
            window.clearTimeout(timeout);
            if (stripeCheckoutInitToken !== initToken) {
              if (checkout && typeof checkout.destroy === "function") {
                checkout.destroy();
              }
              return;
            }

            stripeEmbeddedCheckout = checkout;
            $mount.empty().removeClass("storevoiceai-stripe-checkout--loading");
            checkout.mount("#storevoiceai-stripe-checkout");
          })
          .catch(failStripeCheckout);
      })
      .fail(function (xhr) {
        logStripeTopUpDebug("session request failed", {
          status: xhr && xhr.status,
          message: xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message,
        });
        failStripeCheckout(new Error(getAjaxMessage(xhr.responseJSON && xhr.responseJSON.data, "Stripe checkout could not be started.")));
      });
  }

  function ensureStripeTopUpModal() {
    let $modal = $("#storevoiceai-stripe-modal");
    if ($modal.length) return $modal;

    $modal = $(`
      <div id="storevoiceai-stripe-modal" class="storevoiceai-stripe-modal" role="dialog" aria-modal="true" aria-hidden="true" hidden>
        <div class="storevoiceai-stripe-modal__panel">
          <div class="storevoiceai-stripe-modal__header">
            <div>
              <h2>Top up wallet</h2>
              <p><span class="storevoiceai-stripe-modal__amount"></span> will be credited after Stripe confirms payment.</p>
            </div>
            <button type="button" class="storevoiceai-stripe-modal__close" aria-label="Close checkout">&times;</button>
          </div>
          <div id="storevoiceai-stripe-checkout" class="storevoiceai-stripe-checkout"></div>
        </div>
      </div>
    `);

    $("body").append($modal);
    return $modal;
  }

  function closeStripeTopUpModal(options) {
    stripeCheckoutInitToken += 1;

    if (stripeEmbeddedCheckout && typeof stripeEmbeddedCheckout.destroy === "function") {
      stripeEmbeddedCheckout.destroy();
    }
    stripeEmbeddedCheckout = null;

    if (!options || !options.keepButton) {
      setTopUpButtonLoading(activeTopUpButton, false);
      activeTopUpButton = null;
    }

    const $modal = $("#storevoiceai-stripe-modal");
    $("#storevoiceai-stripe-checkout").empty().removeClass("storevoiceai-stripe-checkout--loading");

    if (!options || !options.keepModal) {
      $modal.attr("hidden", true).attr("aria-hidden", "true");
    }
  }

  function showTopUpProcessingNoticeFromReturnUrl() {
    const params = new URLSearchParams(window.location.search);
    if (params.get("storevoiceai_topup") !== "processing") return;

    const amount = Number(params.get("topup_amount") || 0);
    const amountText = amount > 0 ? formatMoney(amount) + " " : "";

    clearTopUpReturnParams();

    window.StoreVoiceAINotice.show({
      target: $("#billing-and-usage"),
      type: "success",
      message: amountText + "top-up payment received. Wallet credit is processing and may take a moment to appear. Use Refresh balance once it's done.",
      position: "before",
      autoDismiss: false,
      action: {
        label: "Refresh balance",
        onClick: function () {
          loadMinutesQuota();
          loadWalletTransactions();
        },
      },
    });
  }

  function clearTopUpReturnParams() {
    if (!window.history || typeof window.history.replaceState !== "function") return;

    const url = new URL(window.location.href);
    url.searchParams.delete("storevoiceai_topup");
    url.searchParams.delete("topup_amount");
    window.history.replaceState({}, document.title, url.toString());
  }

  function buildTopUpReturnUrl(amount) {
    const billingUrl = storevoiceaiCallLogger.billing_url || window.location.href;
    const url = new URL(billingUrl, window.location.origin);
    url.searchParams.set("storevoiceai_topup", "processing");
    url.searchParams.set("topup_amount", String(amount));
    return url.toString();
  }

  function isValidTopUpAmount(amount) {
    return getTopUpOptions().some((option) => Number(option.amount) === Number(amount));
  }

  function getAssistantButtonLabel() {
    const $button = $("#storevoiceai-create-assistant__button");
    const hasAgent = String($button.data("has-agent")) === "1";
    if (!hasAgent) return "Create Agent";

    const assistantName = $("#storevoiceai-asst-name").val().trim();
    return assistantName ? `Update ${assistantName}` : "Update Support Agent";
  }

  function syncAssistantButtonLabel() {
    const $button = $("#storevoiceai-create-assistant__button");
    if (!$button.length || $button.prop("disabled")) return;
    $button.text(getAssistantButtonLabel());
  }

  syncAssistantButtonLabel();
  $("#storevoiceai-asst-name").on("input", syncAssistantButtonLabel);

  // Valid US number in E.164 form: "+1", a 2-9 area-code digit, then 9 more digits.
  const US_E164_REGEX = /^\+1[2-9]\d{9}$/;

  function setTransferPhoneError(message) {
    const $input = $("#storevoiceai-asst-transfer-phone");
    const $error = $("#storevoiceai-asst-transfer-phone-error");
    if (message) {
      $input.addClass("storevoiceai-input-error").attr("aria-invalid", "true");
      $error.text(message).removeAttr("hidden");
    } else {
      $input.removeClass("storevoiceai-input-error").removeAttr("aria-invalid");
      $error.text("").attr("hidden", true);
    }
  }

  // Clear the validation error as soon as the user starts correcting it.
  $("#storevoiceai-asst-transfer-phone").on("input", function () {
    setTransferPhoneError("");
  });

  let hasProvisionedPhoneNumber = false;

  function setTransferFieldsEnabled(enabled) {
    const $card = $(".storevoiceai-transfer-card").first();
    if (!$card.length) return;

    $card.find("input, button").prop("disabled", !enabled);
  }

  function scrollToPhoneSetup() {
    const phoneSetup = document.querySelector(".phone-setup");
    if (!phoneSetup) return;

    phoneSetup.scrollIntoView({ behavior: "smooth", block: "start" });
    const areaCode = document.getElementById("storevoiceai-area-code");
    if (areaCode && !areaCode.disabled) {
      setTimeout(function () {
        areaCode.focus({ preventScroll: true });
      }, 350);
    }
  }

  function renderTransferGate(hasPhoneNumber) {
    const $card = $(".storevoiceai-transfer-card").first();
    if (!$card.length) return;

    $card.find(".phone-subscription-wall").remove();

    if (storevoiceaiCallLogger.is_subscribed && hasPhoneNumber) {
      $card.removeClass("storevoiceai-transfer-card--locked");
      setTransferFieldsEnabled(true);
      return;
    }

    const safeUrl = getSafePricingUrl();
    const $wall = $('<div class="phone-subscription-wall" role="status">');
    const $content = $('<div class="phone-subscription-wall__content">');
    const lockedForPlan = !storevoiceaiCallLogger.is_subscribed;

    $content.append(
      '<span class="dashicons dashicons-lock phone-subscription-wall__icon" aria-hidden="true"></span>',
    );
    $content.append($("<strong>").text("Can't use this feature"));
    $content.append(
      $("<p>").text(
        lockedForPlan
          ? "Subscribe to transfer unresolved calls to a human phone number during your selected working hours."
          : "Transfer only works for calls made to your StoreVoiceAI phone number. Get a phone number first, then add your transfer details here.",
      ),
    );

    if (lockedForPlan && safeUrl) {
      $content.append(
        $('<a class="button button-primary phone-subscription-wall__action">')
          .attr("href", safeUrl)
          .text("View pricing"),
      );
    } else if (!lockedForPlan) {
      $content.append(
        $('<button type="button" class="button button-primary phone-subscription-wall__action">')
          .text("Get a phone number")
          .on("click", scrollToPhoneSetup),
      );
    }

    $wall.append($content);
    $card.addClass("storevoiceai-transfer-card--locked");
    setTransferFieldsEnabled(false);
    $card.append($wall);
  }

  function initializeWorkingHoursSettings() {
    const toggle = document.getElementById("storevoiceai-working-hours-toggle");
    const panel = document.getElementById("storevoiceai-working-hours-panel");
    const greetingCard = document.getElementById("storevoiceai-greeting-card");
    const ownerEmailCard = document.getElementById("storevoiceai-owner-email-card");
    const leftColumn = document.getElementById("storevoiceai-settings-left-column");
    const rightColumn = document.getElementById("storevoiceai-settings-right-column");
    if (!toggle || !panel) return;

    const syncGreetingPlacement = (enabled) => {
      if (!greetingCard || !leftColumn || !rightColumn) return;

      const target = enabled ? leftColumn : rightColumn;
      if (greetingCard.parentElement !== target) {
        target.appendChild(greetingCard);
      }
      if (ownerEmailCard && ownerEmailCard.parentElement !== target) {
        target.appendChild(ownerEmailCard);
      } else if (ownerEmailCard && ownerEmailCard.previousElementSibling !== greetingCard) {
        target.insertBefore(ownerEmailCard, greetingCard.nextSibling);
      }
    };

    const setEnabled = (enabled) => {
      toggle.classList.toggle("storevoiceai-switch--on", enabled);
      toggle.setAttribute("aria-checked", enabled ? "true" : "false");
      if (enabled) {
        panel.removeAttribute("hidden");
      } else {
        panel.setAttribute("hidden", true);
      }
      syncGreetingPlacement(enabled);
    };

    setEnabled(toggle.getAttribute("aria-checked") === "true");

    toggle.addEventListener("click", function () {
      setEnabled(toggle.getAttribute("aria-checked") !== "true");
    });

    panel.querySelectorAll(".storevoiceai-hours__row").forEach((row) => {
      const offBox = row.querySelector(".storevoiceai-hours__off-box");
      const times = row.querySelector(".storevoiceai-hours__times");
      if (!offBox || !times) return;

      const syncTimes = () => {
        times.style.visibility = offBox.checked ? "hidden" : "visible";
      };

      offBox.addEventListener("change", syncTimes);
      syncTimes();
    });
  }

  function serializeWorkingHoursSettings() {
    const hours = {};
    document.querySelectorAll(".storevoiceai-hours__row[data-day]").forEach((row) => {
      const day = row.getAttribute("data-day");
      const offBox = row.querySelector(".storevoiceai-hours__off-box");
      const startInput = row.querySelector(".storevoiceai-hours__start");
      const endInput = row.querySelector(".storevoiceai-hours__end");

      if (!day || !offBox || !startInput || !endInput) return;

      hours[day] = {
        off: offBox.checked,
        start: startInput.value || "09:00",
        end: endInput.value || "17:00",
      };
    });
    return hours;
  }

  initializeWorkingHoursSettings();
  renderTransferGate(false);
  $(document).on("storevoiceai:phoneNumbersChanged", function (_event, detail) {
    hasProvisionedPhoneNumber = !!(detail && detail.hasPhoneNumber);
    $(".storevoiceai-transfer-card")
      .first()
      .removeClass("storevoiceai-transfer-card--loading");
    renderTransferGate(hasProvisionedPhoneNumber);
  });

  // Create/Update an agent
  $("#storevoiceai-create-assistant__button").click(function (e) {
    e.preventDefault();

    const assistantName = $("#storevoiceai-asst-name").val().trim();
    const firstMessage = $("#storevoiceai-asst-greeting").val().trim();
    const voiceId = $("#voiceDropdown").val(); // read at click time, dropdown is populated by now
    const isSubscribed = !!storevoiceaiCallLogger.is_subscribed;
    const canUseTransfer = isSubscribed && hasProvisionedPhoneNumber;
    const transferPhone = canUseTransfer ? $("#storevoiceai-asst-transfer-phone").val().trim() : "";
    const workingHoursToggle = document.getElementById("storevoiceai-working-hours-toggle");
    const escalationMode =
      canUseTransfer && workingHoursToggle && workingHoursToggle.getAttribute("aria-checked") === "true"
        ? "hours"
        : "always";
    const workingHours = canUseTransfer ? serializeWorkingHoursSettings() : {};
    const shippingPolicy = $("#storevoiceai-shipping-policy").val();
    const returnsPolicy = $("#storevoiceai-returns-policy").val();
    const ownerEmail = $("#storevoiceai-owner-email").val();
    const $button = $(this);
    const $responseElement = $(".storevoiceai-asst-response");

    if (!assistantName) {
      alert("Please enter an assistant name.");
      return;
    }

    if (!voiceId) {
      alert("Please wait for voices to load before saving.");
      return;
    }

    // Transfer phone is optional for paid users, but when set it must be a valid US E.164 number.
    setTransferPhoneError("");
    if (canUseTransfer && transferPhone && !US_E164_REGEX.test(transferPhone)) {
      setTransferPhoneError(
        "Enter a valid US phone number in E.164 format, e.g. +14155234567."
      );
      $("#storevoiceai-asst-transfer-phone").focus();
      return;
    }

    $button.text("Saving...").prop("disabled", true);

    const payload = {
      action: "storevoiceai_create_voice_assistant",
      assistant_name: assistantName,
      assistant_first_message: firstMessage,
      assistant_voice: voiceId,
      shipping_policy: shippingPolicy,
      returns_policy: returnsPolicy,
      owner_email: ownerEmail,
      nonce: storevoiceaiCallLogger.nonce,
    };

    if (canUseTransfer) {
      payload.assistant_transfer_phone = transferPhone;
      payload.escalation_mode = escalationMode;
      payload.working_hours = JSON.stringify(workingHours);
    }

    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: payload,
      success: function (response) {
        if (response.success) {
          $button.data("saved-voice", voiceId);
          $responseElement.empty();
          window.StoreVoiceAINotice.show({
            target: $button.closest("p, .storevoiceai-asst-form, form").length
              ? $button.closest("p, .storevoiceai-asst-form, form").last()
              : $button,
            type: "success",
            message: "Support agent updated successfully.",
            position: "after",
          });

          setTimeout(function () {
            location.reload();
          }, 1200);
        } else {
          window.StoreVoiceAINotice.show({
            target: $button,
            type: "error",
            message: "Failed to update the agent. Please re-try.",
            position: "after",
            duration: 10000,
          });
        }
      },
      error: function () {
        window.StoreVoiceAINotice.show({
          target: $button,
          type: "error",
          message: "Failed to update the agent. Please re-try.",
          position: "after",
          duration: 10000,
        });
      },
      complete: function () {
        $button.text(getAssistantButtonLabel()).prop("disabled", false);
      },
    });
  });

  function renderMinutesQuotaSkeleton() {
    $("#storevoiceai-billing-summary").empty().attr("hidden", true);
    $("#storevoiceai-minutes-quota").html(renderBillingCardSkeleton());
    $("#storevoiceai-wallet-card").html(renderBillingCardSkeleton());
    $("#storevoiceai-rate-card").html(renderBillingCardSkeleton());
    $("#storevoiceai-usage-totals").html(renderUsageTotalsHtml([]));
    $("#storevoiceai-usage-breakdown").html(renderBillingTableSkeleton());
    $("#storevoiceai-wallet-transactions")
      .closest(".billing-table-panel")
      .removeClass("billing-table-panel--wallet-empty");
    $("#storevoiceai-wallet-transactions").html(renderBillingTableSkeleton());
  }

  function renderBillingCardSkeleton() {
    return `
      <span class="minutes-skeleton minutes-skeleton--title">&nbsp;</span>
      <span class="minutes-skeleton minutes-skeleton--usage">&nbsp;</span>
      <span class="minutes-skeleton minutes-skeleton--line">&nbsp;</span>
      <div class="minutes-progress"><div class="minutes-progress__bar minutes-skeleton"></div></div>
    `;
  }

  function renderBillingTableSkeleton() {
    return `
      <div class="billing-table-skeleton">
        <span class="minutes-skeleton minutes-skeleton--line">&nbsp;</span>
        <span class="minutes-skeleton minutes-skeleton--line">&nbsp;</span>
        <span class="minutes-skeleton minutes-skeleton--line">&nbsp;</span>
        <span class="minutes-skeleton minutes-skeleton--line">&nbsp;</span>
      </div>
    `;
  }

  function loadMinutesQuota() {
    renderMinutesQuotaSkeleton();
    var $target = $("#storevoiceai-minutes-quota");
    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_get_voice_usage",
        nonce: storevoiceaiCallLogger.nonce,
      },
      success: function (response) {
        if (response.success) {
          renderMinutesQuota(response.data);
        } else {
          showError(response.data, $target, { title: "Couldn’t load minutes usage", onRetry: loadMinutesQuota });
        }
      },
      error: function (xhr) {
        showError(xhr.responseJSON || "Couldn’t reach the StoreVoiceAI service.", $target, {
          title: "Couldn’t load minutes usage",
          onRetry: loadMinutesQuota,
        });
      },
    });
  }

  function loadMinutesUsageAlert() {
    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_get_voice_usage",
        nonce: storevoiceaiCallLogger.nonce,
      },
      success: function (response) {
        if (response.success) {
          renderMinutesUsageAdminNotice(response.data);
        }
      },
    });
  }

  function loadMinutesUsageState() {
    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_get_voice_usage",
        nonce: storevoiceaiCallLogger.nonce,
      },
      success: function (response) {
        if (!response.success) return;

        const quota = (response.data && response.data.quota) || {};
        const allowed = Number(quota.minutesAllowed || 0);
        const used = Number(quota.minutesUsed || 0);
        const pct = allowed > 0 ? Math.min(100, Math.round((used / allowed) * 100)) : 0;
        updateCallWidgetVisibility(getMinutesUsageAlertState(allowed, pct) !== "exhausted");
      },
    });
  }

  function renderMinutesQuota(data) {
    const quota = (data && data.quota) || {};
    const used = Number(quota.minutesUsed || 0);
    const allowed = Number(quota.minutesAllowed || 0);
    // Derive remaining locally so the card stays internally consistent with the
    // "used / allowed" line and the progress bar, even if the backend returns a
    // stale minutesRemaining value.
    const remaining = Math.max(allowed - used, 0);
    const pct = allowed > 0 ? Math.min(100, Math.round((used / allowed) * 100)) : 0;

    const usageAlertState = getMinutesUsageAlertState(allowed, pct);
    const billingState = getBillingUsageState(allowed, pct);
    const billing = getBillingSnapshot(data, used, allowed, remaining);
    updateCallWidgetVisibility(usageAlertState !== "exhausted");
    renderMinutesUsageAdminNotice(data);

    renderBillingSummary(billing, billingState);
    renderPlanMinutesCard(billing, billingState);
    renderWalletCard(billing);
    renderRateCard(billing);
    renderBillingPaywalls();
  }

  function getBillingSnapshot(data, used, allowed, remaining) {
    const source = data || {};
    const rate = pickNumber(source, [
      "overageRatePerMinute",
      "extraMinuteRate",
      "ratePerMinute",
      "wallet.ratePerMinute",
      "wallet.minuteRate",
      "overageWallet.ratePerMinute",
    ], 0.05);
    const walletBalance = pickNumber(source, [
      "walletBalance",
      "walletBalanceUsd",
      "wallet.balance",
      "wallet.balanceUsd",
      "overageWallet.balance",
      "overageWallet.balanceUsd",
    ], 0);
    const extraMinutes = pickNumber(source, [
      "estimatedExtraMinutes",
      "extraMinutes",
      "wallet.estimatedExtraMinutes",
      "overageWallet.estimatedExtraMinutes",
    ], rate <= 0 ? 0 : Math.floor(walletBalance / rate));
    const renewsAt = pickValue(source, [
      "quota.windowEnd",
      "quota.renewsAt",
      "quota.renewalDate",
      "quota.periodEnd",
      "planRenewsAt",
      "currentPeriodEnd",
    ], "");

    return {
      used: used,
      allowed: allowed,
      remaining: remaining,
      pct: allowed > 0 ? Math.min(100, Math.round((used / allowed) * 100)) : 0,
      rate: rate,
      walletBalance: walletBalance,
      extraMinutes: extraMinutes,
      renewsAt: renewsAt,
      hasWalletData: true,
      topUps: getTopUpOptions(),
    };
  }

  function renderBillingSummary(billing, state) {
    const $summary = $("#storevoiceai-billing-summary");
    if (state === "normal") {
      $summary
        .empty()
        .removeClass("billing-summary-panel--warning billing-summary-panel--exhausted")
        .attr("hidden", true);
      return;
    }

    const title = state === "exhausted"
      ? "Your included plan minutes are finished."
      : "Your included plan minutes are almost finished.";
    const body = state === "exhausted"
      ? "Additional call minutes are now charged from your wallet at " + formatMoney(billing.rate) + " per minute."
      : "Top up your overage wallet now so calls can continue after included minutes run out.";
    const balanceText = billing.hasWalletData ? formatMoney(billing.walletBalance) : "Pending";
    const extraText = billing.extraMinutes === null ? "Pending" : formatWholeNumber(billing.extraMinutes);

    $summary
      .removeAttr("hidden")
      .toggleClass("billing-summary-panel--warning", state === "warning")
      .toggleClass("billing-summary-panel--exhausted", state === "exhausted")
      .html(`
        <div class="billing-summary-icon" aria-hidden="true">
          <span class="dashicons dashicons-warning"></span>
        </div>
        <div class="billing-summary-copy">
          <strong>${escapeHtml(title)}</strong>
          <span>${escapeHtml(body)}</span>
        </div>
        <div class="billing-summary-stats" aria-label="Billing summary">
          <div>
            <span>Wallet balance</span>
            <strong>${escapeHtml(balanceText)}</strong>
          </div>
          <div>
            <span>Estimated extra minutes</span>
            <strong>${escapeHtml(extraText)}</strong>
          </div>
          <div class="billing-status-pill ${state === "exhausted" ? "billing-status-pill--active" : ""}">
            <span aria-hidden="true"></span>${state === "exhausted" ? "Overage active" : "Minutes low"}
          </div>
        </div>
      `);
  }

  function renderPlanMinutesCard(billing, state) {
    const isSubscribed = !!storevoiceaiCallLogger.is_subscribed;
    const renewsAt = isSubscribed
      ? (billing.renewsAt ? "Plan renews on " + formatDisplayDate(billing.renewsAt) : "Renewal date pending")
      : "Free plan";
    const exhausted = state === "exhausted";
    const safeUrl = getSafePricingUrl();
    // Free-plan users don't have a renewal cycle to top up against, so the plan
    // card swaps the "minutes remaining" line for a Subscribe call to action.
    const subscribeCta = !isSubscribed && safeUrl
      ? `<a class="button button-primary minutes-subscribe" href="${escapeHtml(safeUrl)}">Subscribe for more minutes</a>`
      : "";
    $("#storevoiceai-minutes-quota").html(`
      <div class="billing-card-layout billing-card-layout--plan">
        <span class="billing-card-icon billing-card-icon--orange" aria-hidden="true"><span class="dashicons dashicons-clock"></span></span>
        <div class="billing-card-content">
          <div class="billing-card-title-row">
            <h3>Plan Minutes</h3>
            <p>${escapeHtml(renewsAt)}</p>
          </div>
          <p class="minutes-usage"><strong>${formatWholeNumber(billing.used)}</strong> / ${formatWholeNumber(billing.allowed)} used</p>
        </div>
      </div>
      <div class="minutes-progress" role="img" aria-label="${billing.pct}% of included minutes used">
        <div class="minutes-progress__bar ${state === "warning" ? "minutes-progress__bar--warning" : ""} ${exhausted ? "minutes-progress__bar--exhausted" : ""}" style="width:${billing.pct}%"></div>
      </div>
      <p class="minutes-remaining ${exhausted ? "minutes-remaining--exhausted" : ""}">
        ${exhausted ? "All included minutes are used up" : formatWholeNumber(billing.remaining) + " included minutes remaining"}
      </p>
      ${subscribeCta}
    `);
  }

  function renderWalletCard(billing) {
    const topUps = billing.topUps;
    const selected = topUps.find((option) => Number(option.amount) === 25) || topUps[0] || {};
    const gatewayState = getTopUpGatewayState();
    const topUpsHidden = gatewayState.mode === "hidden";
    const topUpsDisabled = gatewayState.mode === "disabled";
    const buttons = topUps.map((option) => {
      const amount = Number(option.amount || 0);
      const isActive = amount === Number(selected.amount || 0);
      return `<button type="button" class="billing-topup-option ${isActive ? "is-active" : ""}" data-amount="${amount}" aria-pressed="${isActive ? "true" : "false"}" ${topUpsDisabled ? "disabled" : ""}>${escapeHtml(option.label || "$" + amount)}</button>`;
    }).join("");
    const topUpButton = topUpsHidden
      ? ""
      : `<a class="button button-primary storevoiceai-topup-checkout ${topUpsDisabled ? "is-disabled" : ""}" href="#" data-selected-amount="${Number(selected.amount || 0)}" aria-disabled="${topUpsDisabled ? "true" : "false"}">Top Up</a>`;
    const topUpRow = topUpsHidden
      ? ""
      : `
      <div class="billing-topup-row ${topUpsDisabled ? "billing-topup-row--disabled" : ""}">
        <span>Quick top-up</span>
        ${topUpsDisabled ? `<p class="billing-topup-message">${escapeHtml(gatewayState.message)}</p>` : `<div class="billing-topup-options">${buttons}</div>`}
      </div>`;

    $("#storevoiceai-wallet-card").html(`
      <div class="billing-card-header billing-card-header--action">
        <span class="billing-card-icon billing-card-icon--green" aria-hidden="true"><span class="dashicons dashicons-money-alt"></span></span>
        <div class="billing-wallet-main">
          <h3>Overage Wallet</h3>
          <p class="billing-wallet-balance">
            <strong>${billing.hasWalletData ? formatMoney(billing.walletBalance) : "Pending"}</strong>
            <span>available</span>
          </p>
          <p class="billing-wallet-note">Used only after your plan minutes run out.</p>
        </div>
        ${topUpButton}
      </div>
      ${topUpRow}
    `);
  }

  function renderRateCard(billing) {
    $("#storevoiceai-rate-card").html(`
      <div class="billing-card-layout billing-card-layout--rate">
        <span class="billing-card-icon billing-card-icon--purple" aria-hidden="true"><span class="dashicons dashicons-tickets-alt"></span></span>
        <div class="billing-card-content">
          <div class="billing-card-title-row">
            <h3>Extra Minute Rate</h3>
            <span class="dashicons dashicons-info-outline billing-info-icon" aria-label="Overage minute rate"></span>
          </div>
          <p class="billing-rate"><strong>${formatMoney(billing.rate)}</strong> / minute</p>
          <p class="billing-rate-note">Charged after included minutes are used.</p>
        </div>
      </div>
    `);
  }

  // Top-up and overage rate only apply to subscribed plans, so free-plan users
  // get a paywall over those two cards. Reuses the same .phone-subscription-wall
  // treatment as the settings page (phone numbers / transfer).
  function renderBillingPaywalls() {
    const cards = [
      { selector: "#storevoiceai-wallet-card", title: "Top up available on paid plans", message: "Top up your overage wallet once you're on a paid plan. Subscribe to add wallet credit for calls beyond your included minutes." },
      { selector: "#storevoiceai-rate-card", title: "Extra minute pricing available on paid plans", message: "Extra minute pricing applies to paid plans. Subscribe to keep calls running at this rate after your included minutes are used." },
    ];

    cards.forEach(function (card) {
      const $card = $(card.selector);
      if (!$card.length) return;

      $card.find(".phone-subscription-wall").remove();
      $card.removeClass("billing-card--locked");

      if (storevoiceaiCallLogger.is_subscribed) return;

      const safeUrl = getSafePricingUrl();
      const $wall = $('<div class="phone-subscription-wall" role="status">');
      const $content = $('<div class="phone-subscription-wall__content">');

      $content.append(
        '<span class="dashicons dashicons-lock phone-subscription-wall__icon" aria-hidden="true"></span>',
      );
      $content.append($("<strong>").text(card.title));
      $content.append($("<p>").text(card.message));

      if (safeUrl) {
        $content.append(
          $('<a class="button button-primary phone-subscription-wall__action">')
            .attr("href", safeUrl)
            .text("Upgrade plan"),
        );
      }

      $wall.append($content);
      $card.addClass("billing-card--locked").append($wall);
    });
  }

  function renderMinutesUsageAdminNotice(data) {
    const quota = (data && data.quota) || {};
    const allowed = Number(quota.minutesAllowed || 0);
    const used = Number(quota.minutesUsed || 0);
    const pct = allowed > 0 ? Math.min(100, Math.round((used / allowed) * 100)) : 0;

    const usageAlertState = getMinutesUsageAlertState(allowed, pct);
    updateCallWidgetVisibility(usageAlertState !== "exhausted");

    $(".storevoiceai-minutes-admin-notice").remove();
    if (!usageAlertState) return;

    const safeUrl = getSafePricingUrl();
    const $notice = $(
      '<div class="notice storevoiceai-minutes-admin-notice" role="alert">' +
        '<p></p>' +
      '</div>'
    ).addClass(usageAlertState === "exhausted" ? "notice-error" : "notice-warning");
    $notice.find("p").append(
      document.createTextNode(getMinutesUsageAlertMessage(pct, usageAlertState))
    );

    if (safeUrl) {
      $notice.find("p").append(
        $('<a class="button button-primary storevoiceai-minutes-admin-notice__action">')
          .attr("href", safeUrl)
          .text("Subscribe now")
      );
    }

    const $syncButton = $("#storevoiceai-sync-calls").first();
    const $heading = $(".wrap h1").first();
    if ($syncButton.length) {
      $syncButton.after($notice);
    } else if ($heading.length) {
      $heading.after($notice);
    } else {
      $(".wrap").first().prepend($notice);
    }
  }

  function getMinutesUsageAlertState(allowed, pct) {
    if (storevoiceaiCallLogger.is_subscribed || allowed <= 0) return "";
    if (pct >= 100) return "exhausted";
    if (pct >= 80) return "warning";
    return "";
  }

  function updateCallWidgetVisibility(visible) {
    $("body").toggleClass("storevoiceai-minutes-exhausted", !visible);
    $("#retell-wrapper").prop("hidden", !visible);
  }

  function isPricingPage() {
    return new URLSearchParams(window.location.search).get("page") ===
      "storevoiceai-for-woocommerce-pricing";
  }

  function getMinutesUsageAlertMessage(pct, state) {
    if (state === "exhausted") {
      return "You've used all of your call minutes. Customers are no longer able to make calls. Subscribe now to restore calling.";
    }

    return "You've used " + pct + "% of your call minutes. Subscribe now to avoid calls being cut off when your minutes run out.";
  }

  function getSafePricingUrl() {
    const rawUrl = storevoiceaiCallLogger.pricing_url || "";
    if (!rawUrl) return "";

    try {
      const parsed = new URL(rawUrl, window.location.origin);
      if (parsed.protocol === "http:" || parsed.protocol === "https:") {
        return parsed.toString();
      }
    } catch (e) {
      return "";
    }

    return "";
  }

  function getBillingUsageState(allowed, pct) {
    if (allowed <= 0) return "normal";
    if (pct >= 100) return "exhausted";
    if (pct >= 80) return "warning";
    return "normal";
  }

  function getActiveMinutesRangeDays() {
    const $active = $(".minutes-range-tab.is-active").first();
    const days = parseInt($active.attr("data-range-days"), 10);
    return Number.isFinite(days) && days > 0 ? days : 30;
  }

  let dailyUsageChart = null;

  function renderDailyUsagePlotSkeleton() {
    const $canvas = $(".billing-chart-canvas");
    if (!$canvas.length || $canvas.find(".billing-chart-skeleton").length) return;
    const heights = [55, 80, 40, 95, 65, 75, 50, 88, 60, 70, 45, 82];
    const bars = heights
      .map((h) => `<span class="billing-chart-skeleton__bar" style="height:${h}%"></span>`)
      .join("");
    $canvas.append(`<div class="billing-chart-skeleton" role="status" aria-label="Loading usage chart">${bars}</div>`);
  }

  function loadDailyUsagePlot(days) {
    renderDailyUsagePlotSkeleton();
    $("#storevoiceai-usage-breakdown").html(renderBillingTableSkeleton());
    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_get_voice_usage_daily",
        nonce: storevoiceaiCallLogger.nonce,
        days: days || 30,
      },
      success: function (response) {
        const days = (response && response.success && response.data && response.data.days) || [];
        renderDailyUsagePlot(days);
      },
      error: function () {
        renderDailyUsagePlot([]);
      },
    });
  }

  function renderDailyUsagePlot(days) {
    const canvas = document.getElementById("storevoiceai-minutes-plot");
    if (!canvas) return;
    $(".billing-chart-skeleton").remove();
    if (dailyUsageChart) {
      dailyUsageChart.destroy();
      dailyUsageChart = null;
    }
    const rows = normalizeUsageDays(days);
    renderUsageTotals(rows);
    renderUsageBreakdown(rows);

    // The table and totals above are populated from the same data, so even when
    // Chart.js is unavailable we still surface the numbers and just swap the
    // canvas for a notice instead of leaving the skeleton spinning forever.
    if (typeof Chart === "undefined") {
      $(canvas).hide();
      const $canvasWrap = $(".billing-chart-canvas");
      $canvasWrap.find(".billing-chart-unavailable").remove();
      $canvasWrap.append(
        $('<p class="billing-chart-unavailable">').text(
          "The usage chart couldn't be loaded. Please refresh the page to try again.",
        ),
      );
      return;
    }
    $(canvas).show();
    $(".billing-chart-canvas").find(".billing-chart-unavailable").remove();

    const labels = rows.map((d) => d.label);
    dailyUsageChart = new Chart(canvas, {
      type: "bar",
      data: {
        labels: labels,
        datasets: [
          {
            label: "Included Minutes",
            data: rows.map((d) => d.includedMinutes),
            backgroundColor: "#1f6feb",
            borderRadius: 3,
            stack: "minutes",
          },
          {
            label: "Overage Minutes",
            data: rows.map((d) => d.overageMinutes),
            backgroundColor: "#ff7a1a",
            borderRadius: 3,
            stack: "minutes",
          },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: {
            display: true,
            position: "bottom",
            labels: { boxWidth: 12, boxHeight: 12, usePointStyle: true },
          },
          tooltip: {
            callbacks: {
              label: function (context) {
                return context.dataset.label + ": " + formatWholeNumber(context.parsed.y || 0) + " minutes";
              },
            },
          },
        },
        scales: {
          x: { stacked: true, grid: { display: false } },
          y: { stacked: true, beginAtZero: true, title: { display: true, text: "Minutes" } },
        },
      },
    });
  }

  function normalizeUsageDays(days) {
    if (!Array.isArray(days)) return [];

    return days.map((day) => {
      const total = numberFrom(day.minutesUsed ?? day.totalMinutes ?? day.minutes, 0);
      const explicitOverage = pickNumber(day, [
        "overageMinutesUsed",
        "overageMinutes",
        "minutesOverage",
        "walletMinutesUsed",
      ], null);
      let overageMinutes = explicitOverage === null ? 0 : explicitOverage;
      let includedMinutes = pickNumber(day, [
        "includedMinutesUsed",
        "includedMinutes",
        "planMinutesUsed",
        "minutesIncluded",
      ], null);

      if (includedMinutes === null) {
        includedMinutes = Math.max(total - overageMinutes, 0);
      }
      if (total > 0 && includedMinutes === 0 && overageMinutes === 0) {
        includedMinutes = total;
      }

      return {
        date: day.date || day.day || "",
        label: formatChartDate(day.date || day.day || ""),
        includedMinutes: includedMinutes,
        overageMinutes: overageMinutes,
        walletCharged: pickNumber(day, ["walletCharged", "walletCharge", "amountCharged", "overageCharge"], 0),
      };
    });
  }

  function renderUsageTotals(rows) {
    $("#storevoiceai-usage-totals").html(renderUsageTotalsHtml(rows));
  }

  function renderUsageTotalsHtml(rows) {
    const included = rows.reduce((sum, row) => sum + row.includedMinutes, 0);
    const overage = rows.reduce((sum, row) => sum + row.overageMinutes, 0);
    const charged = rows.reduce((sum, row) => sum + row.walletCharged, 0);

    return `
      <div class="billing-usage-total"><span class="billing-dot billing-dot--blue"></span><span>Included minutes used</span><strong>${formatWholeNumber(included)}</strong></div>
      <div class="billing-usage-total"><span class="billing-dot billing-dot--orange"></span><span>Overage minutes used</span><strong>${formatWholeNumber(overage)}</strong></div>
      <div class="billing-usage-total"><span class="billing-dot billing-dot--purple"></span><span>Wallet charges this period</span><strong>${formatMoney(charged)}</strong></div>
    `;
  }

  function renderUsageBreakdown(rows) {
    const $target = $("#storevoiceai-usage-breakdown");
    if (!rows.length) {
      $target.html(renderBillingEmptyState("Daily usage data is not available yet."));
      return;
    }

    const visibleRows = rows.slice(-5).reverse();
    const body = visibleRows.map((row) => `
      <tr>
        <td>${escapeHtml(formatDisplayDate(row.date))}</td>
        <td>${formatWholeNumber(row.includedMinutes)}</td>
        <td>${formatWholeNumber(row.overageMinutes)}</td>
        <td>${formatMoney(row.walletCharged)}</td>
      </tr>
    `).join("");

    $target.html(`
      <table class="billing-table">
        <thead>
          <tr>
            <th>Date</th>
            <th>Included Minutes</th>
            <th>Overage Minutes</th>
            <th>Wallet Charged</th>
          </tr>
        </thead>
        <tbody>${body}</tbody>
      </table>
      <div class="billing-table-footer">Showing ${visibleRows.length} of ${rows.length} days</div>
    `);
  }

  function loadWalletTransactions() {
    $("#storevoiceai-wallet-transactions")
      .closest(".billing-table-panel")
      .removeClass("billing-table-panel--wallet-empty");
    $("#storevoiceai-wallet-transactions").html(renderBillingTableSkeleton());
    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_get_wallet_transactions",
        nonce: storevoiceaiCallLogger.nonce,
      },
      success: function (response) {
        if (response.success) {
          renderWalletTransactions(response.data);
        } else {
          renderWalletTransactions({ transactions: [] }, true);
        }
      },
      error: function () {
        renderWalletTransactions({ transactions: [] }, true);
      },
    });
  }

  function renderWalletTransactions(data, loadFailed) {
    const transactions = normalizeWalletTransactions(data);
    const $target = $("#storevoiceai-wallet-transactions");
    if (!transactions.length) {
      $target.closest(".billing-table-panel").addClass("billing-table-panel--wallet-empty");
      // Distinguish a genuine empty wallet from a failed load so merchants
      // know whether to retry rather than assuming there's simply no data.
      const message = loadFailed
        ? "We couldn't load your wallet transactions. Please refresh to try again."
        : "Wallet transaction data is not available yet.";
      $target.html(renderBillingEmptyState(message, "wallet"));
      return;
    }

    $target.closest(".billing-table-panel").removeClass("billing-table-panel--wallet-empty");
    const visibleRows = transactions.slice(0, 5);
    const body = visibleRows.map((transaction) => {
      const isTopUp = transaction.amount > 0 || /top/i.test(transaction.type);
      return `
        <tr>
          <td>${escapeHtml(formatDisplayDate(transaction.date))}</td>
          <td><span class="billing-transaction-type ${isTopUp ? "billing-transaction-type--topup" : ""}"><span></span>${escapeHtml(transaction.type)}</span></td>
          <td class="${transaction.amount >= 0 ? "billing-amount-positive" : ""}">${formatSignedMoney(transaction.amount)}</td>
          <td>${transaction.balance === null ? "Pending" : formatMoney(transaction.balance)}</td>
        </tr>
      `;
    }).join("");

    $target.html(`
      <table class="billing-table">
        <thead>
          <tr>
            <th>Date</th>
            <th>Type</th>
            <th>Amount</th>
            <th>Balance</th>
          </tr>
        </thead>
        <tbody>${body}</tbody>
      </table>
      <div class="billing-table-footer">Showing ${visibleRows.length} of ${transactions.length} transactions</div>
    `);
  }

  function normalizeWalletTransactions(data) {
    const source = data || {};
    const wallet = source.wallet || source.overageWallet || {};
    const transactions = source.walletTransactions || source.transactions || wallet.transactions || [];
    if (!Array.isArray(transactions)) return [];

    return transactions.map((transaction) => ({
      date: transaction.date || transaction.createdAt || transaction.created_at || "",
      type: transaction.typeLabel || transaction.type || transaction.description || "Wallet transaction",
      amount: numberFrom(transaction.amount ?? transaction.amountUsd ?? transaction.walletAmount, 0),
      balance: pickNumber(transaction, ["balance", "balanceUsd", "endingBalance"], null),
    }));
  }

  function renderBillingEmptyState(message, icon) {
    const iconHtml = icon === "wallet"
      ? `
        <svg class="billing-empty-state__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <path d="M19 7V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-1"></path>
          <path d="M3 7h16a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-5a4 4 0 0 1 0-8h5"></path>
          <circle cx="15" cy="13" r="1"></circle>
        </svg>`
      : "";
    return `<div class="billing-empty-state">${iconHtml}<span>${escapeHtml(message)}</span></div>`;
  }

  function getTopUpOptions() {
    const configured = Array.isArray(storevoiceaiCallLogger.top_up_options)
      ? storevoiceaiCallLogger.top_up_options
      : [];
    if (configured.length) return configured;

    return [10, 25, 50, 100, 200, 500, 1000].map((amount) => ({
      amount: amount,
      label: "$" + amount,
    }));
  }

  function pickValue(source, paths, fallback) {
    for (const path of paths) {
      const value = String(path).split(".").reduce((node, key) => {
        if (!node || typeof node !== "object") return undefined;
        return node[key];
      }, source);
      if (value !== undefined && value !== null && value !== "") return value;
    }
    return fallback;
  }

  function pickNumber(source, paths, fallback) {
    const value = pickValue(source, paths, null);
    if (value === null) return fallback;
    const number = Number(value);
    return Number.isFinite(number) ? number : fallback;
  }

  function numberFrom(value, fallback) {
    const number = Number(value);
    return Number.isFinite(number) ? number : fallback;
  }

  function formatWholeNumber(value) {
    return Math.round(Number(value) || 0).toLocaleString("en-US");
  }

  function formatMoney(value) {
    const number = Number(value) || 0;
    return number.toLocaleString("en-US", { style: "currency", currency: "USD" });
  }

  function formatSignedMoney(value) {
    const number = Number(value) || 0;
    const prefix = number > 0 ? "+" : "";
    return prefix + formatMoney(number);
  }

  function formatDisplayDate(value) {
    if (!value) return "Pending";
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return String(value);
    return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
  }

  function formatChartDate(value) {
    if (!value) return "";
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return String(value);
    return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
  }

  let allCallLogs = null;

  function syncCalls($button) {
    $button.prop("disabled", true).addClass("is-syncing");
    const beforeSyncSignature = getCallLogsSignature(allCallLogs);

    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_sync_calls",
        nonce: storevoiceaiCallLogger.nonce,
      },
      success: function (response) {
        if (response.success) {
          const message =
            (response.data && response.data.message) ||
            "Call sync triggered successfully.";
          window.StoreVoiceAINotice.show({
            target: $(".call-logs-header"),
            type: "success",
            message: message,
            position: "after",
          });
          refreshCallLogsAfterSync(beforeSyncSignature);
          return;
        }

        window.StoreVoiceAINotice.show({
          target: $(".call-logs-header"),
          type: "error",
          message: normalizeErrorMessage(response.data),
          position: "after",
          duration: 10000,
        });
      },
      error: function (xhr) {
        window.StoreVoiceAINotice.show({
          target: $(".call-logs-header"),
          type: "error",
          message: normalizeErrorMessage(
            xhr.responseJSON || "Couldn’t reach the StoreVoiceAI service."
          ),
          position: "after",
          duration: 10000,
        });
      },
      complete: function () {
        $button.prop("disabled", false).removeClass("is-syncing");
      },
    });
  }

  function refreshCallLogsAfterSync(beforeSyncSignature, attempt) {
    const currentAttempt = attempt || 1;
    loadCallLogs({
      loadingText: currentAttempt === 1 ? "Refreshing call logs…" : null,
    }).done(function () {
      const afterSyncSignature = getCallLogsSignature(allCallLogs);
      if (afterSyncSignature !== beforeSyncSignature || currentAttempt >= 4) {
        return;
      }

      setTimeout(function () {
        refreshCallLogsAfterSync(beforeSyncSignature, currentAttempt + 1);
      }, currentAttempt * 1000);
    });
  }

  function getCallLogsSignature(logs) {
    const rows = logs && Array.isArray(logs.data) ? logs.data : [];
    return rows
      .map(function (log) {
        return [log.id, log.startedAt, log.endedAt, log.status, log.endedReason].join(":");
      })
      .join("|");
  }

  function loadCallLogs(options) {
    const opts = options || {};
    var $target = $("#call-logs-table");
    if (opts.loadingText !== null) {
      const loadingText = opts.loadingText || "Loading call logs…";
      $target.html(
        '<div class="storevoiceai-loading"><span class="spinner is-active"></span><span>' +
          escapeHtml(loadingText) +
          "</span></div>"
      );
    }
    return $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_get_call_logs",
        nonce: storevoiceaiCallLogger.nonce,
      },
      success: function (response) {
        if (response.success) {
          allCallLogs = response.data;
          applyCallLogsFilter();
        } else {
          showError(response.data, $target, { title: "Couldn’t load call logs", onRetry: loadCallLogs });
        }
      },
      error: function (xhr) {
        showError(xhr.responseJSON || "Couldn’t reach the StoreVoiceAI service.", $target, {
          title: "Couldn’t load call logs",
          onRetry: loadCallLogs,
        });
      },
    });
  }

  function applyCallLogsFilter() {
    if (!allCallLogs) return;

    const fromVal = $("#call-logs-date-from").val();
    const toVal = $("#call-logs-date-to").val();
    const hasFilter = !!(fromVal || toVal);
    $("#call-logs-date-clear").toggleClass("is-active", hasFilter);

    const fromTs = fromVal ? new Date(fromVal + "T00:00:00").getTime() : null;
    const toTs = toVal ? new Date(toVal + "T23:59:59.999").getTime() : null;

    const source = Array.isArray(allCallLogs.data) ? allCallLogs.data : [];
    const filtered = source.filter((log) => {
      if (!log.startedAt) return !hasFilter;
      const ts = new Date(log.startedAt).getTime();
      if (!isFinite(ts)) return !hasFilter;
      if (fromTs !== null && ts < fromTs) return false;
      if (toTs !== null && ts > toTs) return false;
      return true;
    });

    renderCallLogs(Object.assign({}, allCallLogs, { data: filtered }));
  }

  $(document).on("change", "#call-logs-date-from, #call-logs-date-to", applyCallLogsFilter);
  $(document).on("click", "#call-logs-date-clear", function () {
    $("#call-logs-date-from").val("");
    $("#call-logs-date-to").val("");
    applyCallLogsFilter();
  });

  function renderCallLogs(logs) {
    const table = $('<table class="wp-list-table widefat fixed striped storevoiceai-call-logs-table">');
    const thead = $("<thead>").appendTo(table);
    const tbody = $("<tbody>").appendTo(table);

    thead.append(`
            <tr>
                <th>Date</th>
                <th>Call ID</th>
                <th>Type</th>
                <th>Duration</th>
                <th>Status</th>
                <th class="storevoiceai-actions-col">Actions</th>
            </tr>
        `);

    const rows = (logs && logs.data) || [];
    if (!rows.length) {
      $("#call-logs-table").html(`
        <div class="call-logs-empty">
          <svg class="call-logs-empty__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <path d="M22 16.92V21a1 1 0 0 1-1.11 1 19.86 19.86 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.86 19.86 0 0 1 3.18 4.11 1 1 0 0 1 4.18 3h4.09a1 1 0 0 1 1 .75 12.05 12.05 0 0 0 .57 2.57 1 1 0 0 1-.23 1L7.91 9.09a16 16 0 0 0 6 6l1.77-1.77a1 1 0 0 1 1-.23 12.05 12.05 0 0 0 2.57.57 1 1 0 0 1 .75 1.04z"></path>
          </svg>
          <p class="call-logs-empty__text">No calls match the selected filters.</p>
        </div>
      `);
      return;
    }
    rows.forEach((log) => {
      const row = $("<tr>")
        .addClass("storevoiceai-call-row")
        .attr("data-call-id", log.id)
        .attr("tabindex", "0");
      row.append(`
                <td>${getHumanReadableDateTime(log.startedAt)}</td>
                <td><code>${log.id || ""}</code></td>
                <td>${formatCallType(log.type)}</td>
                <td>${formatCallDuration(log.startedAt, log.endedAt)}</td>
                <td>${formatCallStatus(log.status, log.endedReason)}</td>
                <td class="storevoiceai-actions-col">
                    <button class="button view-details" data-call-id="${log.id}">
                        View Details
                    </button>
                </td>
            `);
      tbody.append(row);
    });

    $("#call-logs-table").html(table);
  }

  function formatCallDuration(startedAt, endedAt) {
    if (!startedAt || !endedAt) return "—";
    const start = new Date(startedAt).getTime();
    const end = new Date(endedAt).getTime();
    if (!isFinite(start) || !isFinite(end) || end < start) return "—";
    return formatDuration(end - start);
  }

  function formatCallTypeText(type) {
    if (!type) return "—";
    return String(type).replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
  }

  function formatCallType(type) {
    const label = formatCallTypeText(type);
    if (label === "—") return label;
    return `<span class="storevoiceai-pill storevoiceai-pill--type">${label}</span>`;
  }

  function formatCallStatus(status, endedReason) {
    if (!status && !endedReason) return "—";
    const statusKey = String(status || "").toLowerCase();
    const reasonKey = String(endedReason || "").toLowerCase();
    let modifier = "storevoiceai-pill--status-ok";
    if (/(error|fail|cancel|busy|no.?answer)/.test(statusKey + " " + reasonKey)) {
      modifier = "storevoiceai-pill--status-bad";
    } else if (statusKey !== "ended" && statusKey !== "completed") {
      modifier = "storevoiceai-pill--status-warn";
    }
    const label = status
      ? String(status).replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
      : String(endedReason).replace(/_/g, " ");
    return `<span class="storevoiceai-pill ${modifier}"><span class="storevoiceai-pill__dot"></span>${label}</span>`;
  }

  // Make the entire row clickable (button click is also delegated below).
  $(document).on("click", ".storevoiceai-call-row", function (e) {
    if ($(e.target).closest(".view-details").length) return;
    const callId = $(this).data("call-id");
    if (callId) loadCallDetails(callId);
  });

  $(document).on("keydown", ".storevoiceai-call-row", function (e) {
    if (e.key !== "Enter" && e.key !== " ") return;
    e.preventDefault();
    const callId = $(this).data("call-id");
    if (callId) loadCallDetails(callId);
  });

  $(document).on("click", ".view-details", function (e) {
    e.stopPropagation();
    const callId = $(this).data("call-id");
    loadCallDetails(callId);
  });

  function loadCallDetails(callId) {
    var $target = $("#call-details-modal");
    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_get_call_details",
        nonce: storevoiceaiCallLogger.nonce,
        call_id: callId,
      },
      success: function (response) {
        if (response.success) {
          showCallDetailsModal(response.data);
        } else {
          $target.show();
          showError(response.data, $target, {
            title: "Couldn’t load call details",
            onRetry: function () { loadCallDetails(callId); },
          });
        }
      },
      error: function (xhr) {
        $target.show();
        showError(xhr.responseJSON || "Couldn’t reach the StoreVoiceAI service.", $target, {
          title: "Couldn’t load call details",
          onRetry: function () { loadCallDetails(callId); },
        });
      },
    });
  }

  function getActiveRangeDays() {
    const $active = $(".dashboard-range-tab.is-active").first();
    const days = parseInt($active.attr("data-range-days"), 10);
    return Number.isFinite(days) && days > 0 ? days : 30;
  }

  let callsOverTimeChart = null;
  let disconnectionReasonsChart = null;
  let callsByCategoryChart = null;
  let escalationBreakdownChart = null;

  function loadDashboardData(days) {
    var $target = $("#call-summary-dashboard");
    $target.html('<div class="storevoiceai-loading"><span class="spinner is-active"></span><span>Loading dashboard…</span></div>');
    $.ajax({
      url: storevoiceaiCallLogger.ajax_url,
      type: "POST",
      data: {
        action: "storevoiceai_get_call_summary",
        nonce: storevoiceaiCallLogger.nonce,
        days: days || 30,
      },
      success: function (response) {
        if (response.success) {
          renderDashboard(response.data);
        } else {
          showError(response.data, $target, {
            title: "Couldn’t load dashboard data",
            onRetry: function () { loadDashboardData(days); },
          });
        }
      },
      error: function (xhr) {
        showError(xhr.responseJSON || "Couldn’t reach the StoreVoiceAI service.", $target, {
          title: "Couldn’t load dashboard data",
          onRetry: function () { loadDashboardData(days); },
        });
      },
    });
  }

  function renderDashboard(data) {
    const summary = data.summary || {};
    const totalCalls = Number(summary.totalCallCount || 0);
    const avgDuration = Number(summary.averageCallDurationMinutes || 0);

    // All resolution rates are already percentages (e.g. 83.3 = 83.3%) and use
    // different denominators, so each card states its own basis. When no calls
    // have been post-call-analyzed yet every value is 0 and the gauges read 0%.
    const resolution = data.resolution || {};
    const res = resolution.resolutionRate || {};
    const esc = resolution.escalationRate || {};
    const def = resolution.deflectionRate || {};
    const orderQueries = Number(resolution.orderQueriesHandled || 0);
    const hoursSaved = Number(resolution.supportTimeSavedHours || 0);

    const dashboard = $("#call-summary-dashboard");
    dashboard.html(`
      <div class="dashboard-grid dashboard-grid--primary">
        <div class="dashboard-card">
          <h3>Total Calls Made</h3>
          <p class="dashboard-number">${totalCalls}</p>
        </div>
        <div class="dashboard-card">
          <h3>Resolution Rate</h3>
          ${renderGauge(res.percentage)}
          <p class="dashboard-sublabel">${Number(res.resolved || 0)} of ${Number(res.nonOrderTotal || 0)} non-order calls resolved</p>
        </div>
        <div class="dashboard-card">
          <h3>Deflection Rate</h3>
          ${renderGauge(def.percentage)}
          <p class="dashboard-sublabel">${Number(def.deflected || 0)} of ${Number(def.total || 0)} calls handled without a human</p>
        </div>
      </div>
      <div class="dashboard-grid dashboard-grid--secondary">
        <div class="dashboard-card">
          <h3>Average Call Duration</h3>
          <p class="dashboard-number">${avgDuration.toFixed(2)} <span class="dashboard-unit">min</span></p>
        </div>
        <div class="dashboard-card">
          <h3>Escalation Rate</h3>
          <p class="dashboard-number">${formatPct(esc.percentage)}%</p>
          <p class="dashboard-sublabel">${Number(esc.escalated || 0)} of ${Number(esc.total || 0)} analyzed calls escalated</p>
        </div>
        <div class="dashboard-card">
          <h3>Order Queries Handled</h3>
          <p class="dashboard-number">${orderQueries}</p>
          <p class="dashboard-sublabel">Order-tracking calls</p>
        </div>
        <div class="dashboard-card">
          <h3>Support Time Saved</h3>
          <p class="dashboard-number">≈ ${formatHours(hoursSaved)} <span class="dashboard-unit">hrs</span></p>
          <p class="dashboard-sublabel">this period</p>
        </div>
      </div>
      <div class="dashboard-charts">
        <div class="dashboard-chart-container">
          <h3>Calls by Category</h3>
          <div class="dashboard-chart-canvas" id="storevoiceai-category-wrapper">
            <canvas id="storevoiceai-calls-by-category"></canvas>
          </div>
        </div>
        <div class="dashboard-chart-container">
          <h3>Escalation Breakdown</h3>
          <div class="dashboard-chart-canvas" id="storevoiceai-escalation-wrapper">
            <canvas id="storevoiceai-escalation-breakdown"></canvas>
          </div>
        </div>
        <div class="dashboard-chart-container">
          <h3>Disconnection Reasons</h3>
          <div class="dashboard-chart-canvas" id="storevoiceai-disconnection-wrapper">
            <canvas id="storevoiceai-disconnection-reasons"></canvas>
          </div>
        </div>
        <div class="dashboard-chart-container">
          <h3>Calls Over Time</h3>
          <div class="dashboard-chart-canvas" id="storevoiceai-calls-over-time-wrapper">
            <canvas id="storevoiceai-calls-over-time"></canvas>
          </div>
        </div>
      </div>
    `);

    renderCallsOverTime(data.callsOverTime || []);
    renderDisconnectionReasons(data.disconnectionReasons || []);
    renderCallsByCategory(resolution.callsByCategory || {});
    renderEscalationBreakdown(resolution.escalationRate || {});
  }

  // Circular progress ring used by every percentage stat. The fill is driven
  // purely by the --gauge-value CSS custom property (a 0–100 number) via
  // conic-gradient.
  function renderGauge(percentage) {
    const pct = Math.max(0, Math.min(100, Number(percentage) || 0));
    return `
      <div class="resolution-gauge" style="--gauge-value: ${pct}" role="img" aria-label="${formatPct(pct)} percent">
        <span class="resolution-gauge__value">${formatPct(pct)}<span class="resolution-gauge__unit">%</span></span>
      </div>`;
  }

  function formatPct(value) {
    const n = Math.round((Number(value) || 0) * 10) / 10;
    return Number.isInteger(n) ? String(n) : n.toFixed(1);
  }

  function formatHours(value) {
    const n = Math.round((Number(value) || 0) * 10) / 10;
    return Number.isInteger(n) ? String(n) : n.toFixed(1);
  }

  function emptyStateHtml(message) {
    return `
      <div class="dashboard-empty">
        <svg class="dashboard-empty__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <path d="M22 16.92V21a1 1 0 0 1-1.11 1 19.86 19.86 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.86 19.86 0 0 1 3.18 4.11 1 1 0 0 1 4.18 3h4.09a1 1 0 0 1 1 .75 12.05 12.05 0 0 0 .57 2.57 1 1 0 0 1-.23 1L7.91 9.09a16 16 0 0 0 6 6l1.77-1.77a1 1 0 0 1 1-.23 12.05 12.05 0 0 0 2.57.57 1 1 0 0 1 .75 1.04z"></path>
          <line x1="2" y1="2" x2="22" y2="22"></line>
        </svg>
        <span>${message}</span>
      </div>`;
  }

  // Shown in a chart card when Chart.js failed to load, so the card never
  // renders as a blank box.
  function chartUnavailableHtml() {
    return emptyStateHtml("Charts couldn't be loaded. Please refresh the page to try again.");
  }

  function renderCallsOverTime(points) {
    if (callsOverTimeChart) {
      callsOverTimeChart.destroy();
      callsOverTimeChart = null;
    }
    const wrapper = document.getElementById("storevoiceai-calls-over-time-wrapper");
    const canvas = document.getElementById("storevoiceai-calls-over-time");
    if (!wrapper || !canvas) return;

    const values = points.map((p) => Number(p.callCount || 0));
    const total = values.reduce((sum, v) => sum + v, 0);

    if (!points.length || total === 0) {
      wrapper.innerHTML = emptyStateHtml("No call activity yet. As calls come in, you'll see them charted over time here.");
      return;
    }

    if (typeof Chart === "undefined") {
      wrapper.innerHTML = chartUnavailableHtml();
      return;
    }

    callsOverTimeChart = new Chart(canvas, {
      type: "bar",
      data: {
        labels: points.map((p) => p.date),
        datasets: [
          {
            label: "Calls",
            data: points.map((p) => Number(p.callCount || 0)),
            backgroundColor: "#2271b1",
          },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: { legend: { display: false } },
        scales: {
          y: { beginAtZero: true, ticks: { precision: 0 }, title: { display: true, text: "Calls" } },
        },
      },
    });
  }

  function renderDisconnectionReasons(reasons) {
    if (disconnectionReasonsChart) {
      disconnectionReasonsChart.destroy();
      disconnectionReasonsChart = null;
    }
    const wrapper = document.getElementById("storevoiceai-disconnection-wrapper");
    const canvas = document.getElementById("storevoiceai-disconnection-reasons");
    if (!wrapper || !canvas) return;

    const values = reasons.map((r) => Number(r.count || 0));
    const total = values.reduce((sum, v) => sum + v, 0);

    if (!reasons.length || total === 0) {
      wrapper.innerHTML = emptyStateHtml("No disconnection data yet. Once calls start coming in, you'll see the breakdown here.");
      return;
    }

    if (typeof Chart === "undefined") {
      wrapper.innerHTML = chartUnavailableHtml();
      return;
    }

    const palette = ["#2271b1", "#46b450", "#e67e22", "#dc3232", "#8e44ad", "#5b6770", "#16a085"];
    const labels = reasons.map((r) => formatDisconnectionReason(r.reason));

    disconnectionReasonsChart = new Chart(canvas, {
      type: "pie",
      data: {
        labels: labels,
        datasets: [
          {
            data: values,
            backgroundColor: labels.map((_, i) => palette[i % palette.length]),
            borderWidth: 1,
          },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: { legend: { position: "right" } },
      },
    });
  }

  function formatDisconnectionReason(reason) {
    if (!reason) return "Unknown";
    return String(reason)
      .replace(/_/g, " ")
      .replace(/\b\w/g, (c) => c.toUpperCase());
  }

  // Horizontal bar of analyzed calls by topic. The six category keys are fixed
  // and zero-filled by the API, so the axis stays stable across periods.
  function renderCallsByCategory(categories) {
    if (callsByCategoryChart) {
      callsByCategoryChart.destroy();
      callsByCategoryChart = null;
    }
    const wrapper = document.getElementById("storevoiceai-category-wrapper");
    const canvas = document.getElementById("storevoiceai-calls-by-category");
    if (!wrapper || !canvas) return;

    const order = ["WISMO", "Return", "Product", "Policy", "FAQ", "Other"];
    const values = order.map((k) => Number(categories[k] || 0));
    const total = values.reduce((sum, v) => sum + v, 0);

    if (total === 0) {
      wrapper.innerHTML = emptyStateHtml("No category data yet. Once calls are analyzed, you'll see how they break down by topic here.");
      return;
    }

    if (typeof Chart === "undefined") {
      wrapper.innerHTML = chartUnavailableHtml();
      return;
    }

    callsByCategoryChart = new Chart(canvas, {
      type: "bar",
      data: {
        labels: order,
        datasets: [
          {
            label: "Calls",
            data: values,
            backgroundColor: "#2271b1",
            borderRadius: 4,
          },
        ],
      },
      options: {
        indexAxis: "y",
        responsive: true,
        maintainAspectRatio: false,
        plugins: { legend: { display: false } },
        scales: {
          x: { beginAtZero: true, ticks: { precision: 0 }, title: { display: true, text: "Calls" } },
        },
      },
    });
  }

  // Doughnut of how escalated calls were handled. Breakdown keys are fixed:
  // "Transfer", "Message taken" (note the space) and "None".
  function renderEscalationBreakdown(escalationRate) {
    if (escalationBreakdownChart) {
      escalationBreakdownChart.destroy();
      escalationBreakdownChart = null;
    }
    const wrapper = document.getElementById("storevoiceai-escalation-wrapper");
    const canvas = document.getElementById("storevoiceai-escalation-breakdown");
    if (!wrapper || !canvas) return;

    const breakdown = escalationRate.breakdown || {};
    const order = ["Transfer", "Message taken", "None"];
    const palette = { Transfer: "#e67e22", "Message taken": "#8e44ad", None: "#46b450" };
    const values = order.map((k) => Number(breakdown[k] || 0));
    const total = values.reduce((sum, v) => sum + v, 0);

    if (total === 0) {
      wrapper.innerHTML = emptyStateHtml("No escalation data yet. Transfers and messages taken will appear here once calls are analyzed.");
      return;
    }

    if (typeof Chart === "undefined") {
      wrapper.innerHTML = chartUnavailableHtml();
      return;
    }

    escalationBreakdownChart = new Chart(canvas, {
      type: "doughnut",
      data: {
        labels: order,
        datasets: [
          {
            data: values,
            backgroundColor: order.map((k) => palette[k]),
            borderWidth: 1,
          },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: { legend: { position: "right" } },
      },
    });
  }

  function showCallDetailsModal(details) {
    const modal = $("#call-details-modal");

    const transcriptHtml = details.transcript
      ? renderTranscript(details.transcript)
      : '<p class="call-detail-empty">No transcript available.</p>';

    const duration = formatCallDuration(details.startedAt, details.endedAt);
    const type = formatCallTypeText(details.type);
    const endedReason = details.endedReason
      ? String(details.endedReason).replace(/_/g, " ")
      : "—";

    modal
      .html(
        `
            <div class="modal-content call-detail-modal">
                <button type="button" class="close" aria-label="Close">&times;</button>
                <header class="call-detail-header">
                    <h2>Call Details</h2>
                </header>
                <div class="call-detail-body">
                    <div class="call-detail-left">
                        <div class="call-detail-meta">
                            <div class="call-detail-meta__item">
                                <span class="call-detail-meta__label">Type</span>
                                <span class="call-detail-meta__value">${escapeHtml(type)}</span>
                            </div>
                            <div class="call-detail-meta__item">
                                <span class="call-detail-meta__label">Duration</span>
                                <span class="call-detail-meta__value">${escapeHtml(duration)}</span>
                            </div>
                            <div class="call-detail-meta__item">
                                <span class="call-detail-meta__label">Started</span>
                                <span class="call-detail-meta__value">${escapeHtml(getHumanReadableDateTime(details.startedAt))}</span>
                            </div>
                            <div class="call-detail-meta__item">
                                <span class="call-detail-meta__label">Ended</span>
                                <span class="call-detail-meta__value">${escapeHtml(getHumanReadableDateTime(details.endedAt))}</span>
                            </div>
                            <div class="call-detail-meta__item">
                                <span class="call-detail-meta__label">Ended reason</span>
                                <span class="call-detail-meta__value">${escapeHtml(endedReason)}</span>
                            </div>
                        </div>

                        ${
                          details.summary
                            ? `
                            <section class="call-detail-section">
                                <h3>Summary</h3>
                                <p class="call-detail-summary">${escapeHtml(details.summary)}</p>
                            </section>
                        `
                            : ""
                        }

                        ${
                          details.recordingUrl
                            ? `
                            <section class="call-detail-section recording-section">
                                <h3>Recording</h3>
                                <audio controls class="call-recording" preload="metadata">
                                    <source src="${escapeHtml(details.recordingUrl)}" type="audio/wav">
                                    ${storevoiceaiCallLogger.i18n.audioNotSupported}
                                </audio>
                            </section>
                        `
                            : ""
                        }
                    </div>
                    <div class="call-detail-right">
                        <section class="call-detail-section transcript-section">
                            <h3>Transcript</h3>
                            <div class="transcript-content">${transcriptHtml}</div>
                        </section>
                    </div>
                </div>
            </div>
        `,
      )
      .show();
  }

  function renderTranscript(transcript) {
    const lines = String(transcript).split(/\r?\n/).filter((l) => l.trim().length > 0);
    return lines
      .map((line) => {
        const match = line.match(/^\s*(agent|user|assistant|customer)\s*:\s*(.*)$/i);
        if (match) {
          const speaker = match[1].toLowerCase();
          const role = speaker === "user" || speaker === "customer" ? "user" : "agent";
          const label = role === "user" ? "Customer" : "Agent";
          return `
            <div class="transcript-line transcript-line--${role}">
              <span class="transcript-speaker">${label}</span>
              <span class="transcript-text">${escapeHtml(match[2])}</span>
            </div>`;
        }
        return `<div class="transcript-line"><span class="transcript-text">${escapeHtml(line)}</span></div>`;
      })
      .join("");
  }

  // Close modal when clicking the close button or outside the modal
  $(document).on("click", ".close, .modal", function (e) {
    if (e.target === this) {
      const audio = $("#call-details-modal audio")[0];
      if (audio) {
        audio.pause();
        audio.currentTime = 0;
      }
      $("#call-details-modal").hide();
    }
  });

  // Render a visible failure state into the section that tried to load data.
  // Falls back to a top-of-page WP notice when no target is supplied so a
  // backend hiccup never leaves the UI silently empty.
  //
  // Usage:
  //   showError(payload)
  //   showError(payload, $target)
  //   showError(payload, $target, { onRetry: fn })
  function showError(payload, $target, options) {
    options = options || {};
    var msg = normalizeErrorMessage(payload);

    if (window.console && typeof console.error === "function") {
      console.error("[StoreVoiceAI]", msg, payload);
    }

    if ($target && $target.length) {
      var $card = $(
        '<div class="storevoiceai-error" role="alert">' +
          '<svg class="storevoiceai-error__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
            '<circle cx="12" cy="12" r="9"/>' +
            '<line x1="12" y1="8" x2="12" y2="13"/>' +
            '<circle cx="12" cy="16.5" r="0.7" fill="currentColor" stroke="none"/>' +
          '</svg>' +
          '<p class="storevoiceai-error__title">' + escapeHtml(options.title || "We couldn’t load this section") + '</p>' +
          '<p class="storevoiceai-error__message"></p>' +
          '<div class="storevoiceai-error__actions"></div>' +
        '</div>'
      );
      $card.find(".storevoiceai-error__message").text(msg);

      var $actions = $card.find(".storevoiceai-error__actions");
      if (typeof options.onRetry === "function") {
        $("<button type='button' class='button button-primary'>Try again</button>")
          .appendTo($actions)
          .on("click", function () { options.onRetry(); });
      }
      $("<a class='button button-secondary' href='" + storevoiceaiCallLogger.settings_url + "'>Open Settings</a>")
        .appendTo($actions);

      $target.empty().append($card);
      return;
    }

    // No target → page-level notice using WP admin styling.
    var $existing = $(".storevoiceai-global-error");
    if ($existing.length) { $existing.remove(); }
    var $notice = $(
      '<div class="notice notice-error is-dismissible storevoiceai-global-error">' +
        '<p></p>' +
      '</div>'
    );
    $notice.find("p").text("StoreVoiceAI: " + msg);
    var $wrap = $(".wrap").first();
    if ($wrap.length) {
      $wrap.prepend($notice);
    } else {
      $("body").prepend($notice);
    }
  }

  function normalizeErrorMessage(payload) {
    if (!payload) return "Something went wrong. Please try again.";
    if (typeof payload === "string") return payload;
    if (typeof payload.message === "string" && payload.message) return payload.message;
    if (payload.data) {
      if (typeof payload.data === "string") return payload.data;
      if (typeof payload.data.message === "string") return payload.data.message;
    }
    try { return JSON.stringify(payload); } catch (e) { return "Something went wrong."; }
  }

  // First-run dashboard spotlight. Walks the user through the key surfaces
  // (call widget, Call Logs, Settings) and marks itself seen via AJAX so we
  // never replay it for the same user.
  function startDashboardTour() {
    var tour = window.storevoiceaiTour || {};
    var i18n = tour.i18n || {};
    var logger = window.storevoiceaiCallLogger || {};

    var steps = [
      {
        target: null,
        title: i18n.welcomeTitle || "Welcome",
        body:  i18n.welcomeBody  || "",
      },
      {
        target: "#retell-wrapper",
        title: i18n.widgetTitle || "Try a call anytime",
        body:  i18n.widgetBody  || "",
      },
      {
        target: '#adminmenu a[href*="page=storevoiceai-for-woocommerce-call-logs"]',
        title: i18n.callLogsTitle || "Call Logs",
        body:  i18n.callLogsBody  || "",
      },
      {
        target: '#adminmenu a[href*="page=storevoiceai-for-woocommerce-settings"]',
        title: i18n.settingsTitle || "Settings",
        body:  i18n.settingsBody  || "",
      },
    ];

    var overlay = document.createElement("div");
    overlay.className = "svai-tour";
    overlay.innerHTML =
      '<div class="svai-tour__dim svai-tour__dim--top"    aria-hidden="true"></div>' +
      '<div class="svai-tour__dim svai-tour__dim--bottom" aria-hidden="true"></div>' +
      '<div class="svai-tour__dim svai-tour__dim--left"   aria-hidden="true"></div>' +
      '<div class="svai-tour__dim svai-tour__dim--right"  aria-hidden="true"></div>' +
      '<div class="svai-tour__ring" aria-hidden="true"></div>' +
      '<div class="svai-tour__card" role="dialog" aria-modal="true" aria-labelledby="svai-tour-title">' +
        '<button type="button" class="svai-tour__close" aria-label="Close tour">&times;</button>' +
        '<span class="svai-tour__step"></span>' +
        '<h3 class="svai-tour__title" id="svai-tour-title"></h3>' +
        '<p class="svai-tour__body"></p>' +
        '<div class="svai-tour__actions">' +
          '<button type="button" class="svai-tour__skip"></button>' +
          '<div class="svai-tour__nav">' +
            '<button type="button" class="button svai-tour__back"></button>' +
            '<button type="button" class="button button-primary svai-tour__next"></button>' +
          '</div>' +
        '</div>' +
      '</div>';
    document.body.appendChild(overlay);

    var dimTop    = overlay.querySelector(".svai-tour__dim--top");
    var dimBottom = overlay.querySelector(".svai-tour__dim--bottom");
    var dimLeft   = overlay.querySelector(".svai-tour__dim--left");
    var dimRight  = overlay.querySelector(".svai-tour__dim--right");
    var ring      = overlay.querySelector(".svai-tour__ring");
    var card      = overlay.querySelector(".svai-tour__card");
    var stepEl    = overlay.querySelector(".svai-tour__step");
    var titleEl   = overlay.querySelector(".svai-tour__title");
    var bodyEl    = overlay.querySelector(".svai-tour__body");
    var skipBtn   = overlay.querySelector(".svai-tour__skip");
    var backBtn   = overlay.querySelector(".svai-tour__back");
    var nextBtn   = overlay.querySelector(".svai-tour__next");
    var closeBtn  = overlay.querySelector(".svai-tour__close");

    var index = 0;
    var rafId = null;
    var markedSeen = false;

    function markSeen() {
      if (markedSeen) return;
      markedSeen = true;
      if (!logger.ajax_url || !logger.nonce) return;
      $.post(logger.ajax_url, {
        action: "storevoiceai_dismiss_dashboard_tour",
        nonce:  logger.nonce,
      });
    }

    function fmtStep(template, current, total) {
      return String(template || "Step %1$d of %2$d")
        .replace("%1$d", current)
        .replace("%2$d", total);
    }

    function findTarget(selector) {
      if (!selector) return null;
      var el = document.querySelector(selector);
      if (!el) return null;
      // Skip targets that have no rendered box (display:none collapses offsetParent).
      var rect = el.getBoundingClientRect();
      if (rect.width === 0 && rect.height === 0) return null;
      return el;
    }

    function setCutout(rect) {
      // Four dim rects frame the target. Robust across browsers and avoids
      // the bridge-polygon clip-path gotchas. Without a target we cover the
      // full viewport with just the top rect (top + bottom together).
      var vw = window.innerWidth;
      var vh = window.innerHeight;

      if (!rect) {
        dimTop.style.cssText    = "top:0;left:0;width:100%;height:100%;";
        dimBottom.style.cssText = "display:none;";
        dimLeft.style.cssText   = "display:none;";
        dimRight.style.cssText  = "display:none;";
        return;
      }

      var pad = 8;
      var x1 = Math.max(0, rect.left   - pad);
      var y1 = Math.max(0, rect.top    - pad);
      var x2 = Math.min(vw, rect.right  + pad);
      var y2 = Math.min(vh, rect.bottom + pad);

      dimTop.style.cssText    = "top:0;left:0;width:100%;height:" + y1 + "px;";
      dimBottom.style.cssText = "top:" + y2 + "px;left:0;width:100%;height:" + Math.max(0, vh - y2) + "px;";
      dimLeft.style.cssText   = "top:" + y1 + "px;left:0;width:" + x1 + "px;height:" + Math.max(0, y2 - y1) + "px;";
      dimRight.style.cssText  = "top:" + y1 + "px;left:" + x2 + "px;width:" + Math.max(0, vw - x2) + "px;height:" + Math.max(0, y2 - y1) + "px;";
    }

    function positionRing(rect) {
      if (!rect) {
        ring.style.display = "none";
        return;
      }
      var pad = 8;
      ring.style.display = "block";
      ring.style.top    = (rect.top - pad) + "px";
      ring.style.left   = (rect.left - pad) + "px";
      ring.style.width  = (rect.width  + pad * 2) + "px";
      ring.style.height = (rect.height + pad * 2) + "px";
    }

    function placeCard(rect) {
      var margin = 16;
      var gap    = 18;
      var vw = window.innerWidth;
      var vh = window.innerHeight;

      // Measure card with no constraints first.
      card.style.transform = "none";
      card.style.top = "0px";
      card.style.left = "0px";
      var cw = card.offsetWidth;
      var ch = card.offsetHeight;

      // No target: center on screen.
      if (!rect) {
        card.dataset.side = "center";
        card.style.top  = Math.max(margin, (vh - ch) / 2) + "px";
        card.style.left = Math.max(margin, (vw - cw) / 2) + "px";
        return;
      }

      // Pick the side with the most room that can fit the card. Order is a
      // soft preference; we still pick the largest if none truly fit.
      var spaces = {
        right:  vw - rect.right  - gap - margin,
        left:   rect.left        - gap - margin,
        bottom: vh - rect.bottom - gap - margin,
        top:    rect.top         - gap - margin
      };
      var preferred = ["right", "left", "bottom", "top"];
      var side = null;
      for (var i = 0; i < preferred.length; i++) {
        var s = preferred[i];
        var needed = (s === "right" || s === "left") ? cw : ch;
        if (spaces[s] >= needed) { side = s; break; }
      }
      if (!side) {
        // Nothing fits; fall back to whichever side has the most absolute room.
        side = Object.keys(spaces).sort(function (a, b) { return spaces[b] - spaces[a]; })[0];
      }

      var top, left;
      if (side === "right") {
        left = rect.right + gap;
        top  = rect.top + (rect.height / 2) - (ch / 2);
      } else if (side === "left") {
        left = rect.left - gap - cw;
        top  = rect.top + (rect.height / 2) - (ch / 2);
      } else if (side === "bottom") {
        top  = rect.bottom + gap;
        left = rect.left + (rect.width / 2) - (cw / 2);
      } else {
        top  = rect.top - gap - ch;
        left = rect.left + (rect.width / 2) - (cw / 2);
      }

      // Clamp into viewport without re-overlapping the target.
      left = Math.max(margin, Math.min(left, vw - cw - margin));
      top  = Math.max(margin, Math.min(top,  vh - ch - margin));

      card.dataset.side = side;
      card.style.top  = top + "px";
      card.style.left = left + "px";
    }

    function render() {
      var step = steps[index];
      stepEl.textContent = fmtStep(i18n.stepLabel, index + 1, steps.length);
      titleEl.textContent = step.title;
      bodyEl.textContent  = step.body;

      backBtn.textContent = i18n.back || "Back";
      skipBtn.textContent = i18n.skip || "Skip tour";
      nextBtn.textContent = (index === steps.length - 1)
        ? (i18n.done || "Got it")
        : (i18n.next || "Next");

      backBtn.style.visibility = index === 0 ? "hidden" : "visible";

      var targetEl = findTarget(step.target);
      if (targetEl) {
        try { targetEl.scrollIntoView({ block: "center", behavior: "instant" }); } catch (e) { targetEl.scrollIntoView(); }
        // Read the rect AFTER scrollIntoView in the next frame for accuracy.
        rafId = requestAnimationFrame(function () {
          var rect = targetEl.getBoundingClientRect();
          setCutout(rect);
          positionRing(rect);
          placeCard(rect);
        });
      } else {
        setCutout(null);
        positionRing(null);
        rafId = requestAnimationFrame(function () { placeCard(null); });
      }
    }

    function finish() {
      if (rafId) cancelAnimationFrame(rafId);
      window.removeEventListener("resize", render);
      window.removeEventListener("scroll", render, true);
      document.removeEventListener("keydown", onKey);
      if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
      markSeen();
    }

    function onKey(e) {
      if (e.key === "Escape") finish();
      else if (e.key === "ArrowRight") nextBtn.click();
      else if (e.key === "ArrowLeft") backBtn.click();
    }

    function advance() {
      if (index === steps.length - 1) { finish(); return; }
      index++;
      render();
    }

    nextBtn.addEventListener("click", advance);
    backBtn.addEventListener("click", function () {
      if (index === 0) return;
      index--;
      render();
    });
    skipBtn.addEventListener("click", finish);
    closeBtn.addEventListener("click", finish);
    document.addEventListener("keydown", onKey);

    // The ring sits over the spotlighted element. Without this, clicking the
    // ring would fall through to the underlying link (e.g. the Call Logs menu
    // item) and navigate away mid-tour. Intercept the click and advance the
    // step instead so the tour stays on rails.
    ring.addEventListener("click", function (e) {
      e.preventDefault();
      e.stopPropagation();
      advance();
    });

    window.addEventListener("resize", render);
    window.addEventListener("scroll", render, true);

    // Mark as seen the moment the tour appears, so a mid-tour navigation
    // (closed tab, accidental click on a non-spotlighted area, etc.) won't
    // replay the tour on the next dashboard visit.
    markSeen();
    render();
  }

});
