/* global chrome */

const _sharkIconUrl = chrome.runtime.getURL("icons/icon48.png");
const SHARK_SVG_14 = `<img style="flex-shrink:0;width:14px;height:14px;" src="${_sharkIconUrl}" alt="">`;
const SHARK_SVG_16 = `<img style="flex-shrink:0;width:16px;height:16px;" src="${_sharkIconUrl}" alt="">`;
const SHARK_SVG_12 = `<img style="flex-shrink:0;width:12px;height:12px;" src="${_sharkIconUrl}" alt="">`;

window.SlotSharkPanel = {
  _panelEl: null,
  _wallets: [],
  _activeWallet: null,
  _activeWallets: [],
  _buyDistribution: "full",
  _buyPresets: [0.5, 1, 2, 5],
  _sellPresets: [10, 25, 50, 100],
  _activePresetValues: null,
  _quickBuyAmount: 0.5,
  _quickBuyTip: null,
  _quickBuyFee: null,
  _quickBuyAntimev: true,
  _isAuthenticated: false,
  _dismissHandler: null,
  _escHandler: null,
  _mountTimeoutId: null,
  _storageListener: null,
  _toastTimers: [],
  _cls: "ss" + Math.random().toString(36).substring(2, 8),
  _color: "#1a8cff",
  _bgColor: "#1a8cff30",

  _apiCall(method, path, body) {
    return new Promise((resolve) => {
      try {
        chrome.runtime.sendMessage(
          { type: "API_CALL", method, path, body },
          (response) => {
            if (chrome.runtime.lastError) {
              resolve({ error: "Extension disconnected — refresh the page" });
              return;
            }
            resolve(response);
          }
        );
      } catch {
        resolve({ error: "Extension disconnected — refresh the page" });
      }
    });
  },

  _apiBatch(calls) {
    return new Promise((resolve) => {
      try {
        chrome.runtime.sendMessage(
          { type: "API_BATCH", calls },
          (response) => {
            if (chrome.runtime.lastError) {
              resolve({ results: calls.map(() => ({ error: "Extension disconnected" })) });
              return;
            }
            resolve(response);
          }
        );
      } catch {
        resolve({ results: calls.map(() => ({ error: "Extension disconnected" })) });
      }
    });
  },

  async init() {
    try {
      const data = await chrome.storage.local.get([
        "slotshark.presets",
        "slotshark.activePreset",
        "slotshark.quickBuyAmount",
        "slotshark.quickBuyTip",
        "slotshark.quickBuyFee",
        "slotshark.quickBuyAntimev",
        "slotshark.activeWallet",
        "slotshark.activeWallets",
        "slotshark.buyDistribution",
      ]);

      const presets = data["slotshark.presets"] || [];
      const activeId = data["slotshark.activePreset"];
      const active = presets.find((p) => p.id === activeId) || presets[0];

      if (active && active.values) {
        this._activePresetValues = active.values;
        this._buyPresets = active.values["buy-amounts"] || this._buyPresets;
        this._sellPresets = active.values["sell-percents"] || this._sellPresets;
      }

      if (data["slotshark.quickBuyAmount"]) {
        this._quickBuyAmount = data["slotshark.quickBuyAmount"];
      } else {
        this._quickBuyAmount = this._buyPresets[0] || 0.5;
      }
      this._quickBuyTip = data["slotshark.quickBuyTip"] ?? null;
      this._quickBuyFee = data["slotshark.quickBuyFee"] ?? null;
      this._quickBuyAntimev = data["slotshark.quickBuyAntimev"] !== false;
      if (data["slotshark.activeWallet"]) {
        this._activeWallet = data["slotshark.activeWallet"];
      }
      this._activeWallets = data["slotshark.activeWallets"] || [];
      this._buyDistribution = data["slotshark.buyDistribution"] || "full";
    } catch { /* storage unavailable */ }

    this._apiCall("GET", "/auth/me").then((resp) => {
      if (resp?.data?.success) {
        this._isAuthenticated = true;
        this._wallets = (resp.data.wallets || []).filter((w) => w.active);
        if (!this._activeWallet && this._wallets.length > 0) {
          this._activeWallet = this._wallets[0].pubkey;
        }
      }
    });

    try {
      if (this._storageListener) {
        chrome.storage.onChanged.removeListener(this._storageListener);
      }
      this._storageListener = (changes) => {
        if (changes["slotshark.presets"] || changes["slotshark.activePreset"]) {
          chrome.storage.local.get([
            "slotshark.presets",
            "slotshark.activePreset",
            "slotshark.quickBuyAmount",
          ]).then((data) => {
            const presets = data["slotshark.presets"] || [];
            const activeId = data["slotshark.activePreset"];
            const active = presets.find((p) => p.id === activeId) || presets[0];
            if (active && active.values) {
              this._activePresetValues = active.values;
              this._buyPresets = active.values["buy-amounts"] || [0.5, 1, 2, 5];
              this._sellPresets = active.values["sell-percents"] || [10, 25, 50, 100];
              if (data["slotshark.quickBuyAmount"]) {
                this._quickBuyAmount = data["slotshark.quickBuyAmount"];
              } else {
                this._quickBuyAmount = this._buyPresets[0] || 0.5;
              }
              this._updateAllButtons();
            }
          });
        }
        if (changes["slotshark.quickBuyAmount"]) {
          this._quickBuyAmount = changes["slotshark.quickBuyAmount"].newValue || 0.5;
          this._updateAllButtons();
        }
        if (changes["slotshark.quickBuyTip"]) this._quickBuyTip = changes["slotshark.quickBuyTip"].newValue ?? null;
        if (changes["slotshark.quickBuyFee"]) this._quickBuyFee = changes["slotshark.quickBuyFee"].newValue ?? null;
        if (changes["slotshark.quickBuyAntimev"]) this._quickBuyAntimev = changes["slotshark.quickBuyAntimev"].newValue !== false;
        if (changes["slotshark.activeWallets"]) this._activeWallets = changes["slotshark.activeWallets"].newValue || [];
        if (changes["slotshark.buyDistribution"]) this._buyDistribution = changes["slotshark.buyDistribution"].newValue || "full";
      };
      chrome.storage.onChanged.addListener(this._storageListener);
    } catch { /* orphaned content script */ }
  },

  destroy() {
    this.removeAll();
    if (this._storageListener) {
      try { chrome.storage.onChanged.removeListener(this._storageListener); } catch {}
      this._storageListener = null;
    }
    if (this._mountTimeoutId) {
      clearTimeout(this._mountTimeoutId);
      this._mountTimeoutId = null;
    }
    this._toastTimers.forEach(clearTimeout);
    this._toastTimers = [];
  },

  _updateAllButtons() {
    const cls = this._cls;
    document.querySelectorAll(`[class*="${cls}"]`).forEach((btn) => {
      const span = btn.querySelector("span");
      if (span && !isNaN(parseFloat(span.textContent))) {
        span.textContent = this._quickBuyAmount;
      }
    });
  },

  removeAll() {
    const cls = this._cls;
    document.querySelectorAll(`[class*="${cls}"]`).forEach((el) => el.remove());
    document.querySelectorAll(".ss-toast").forEach((el) => el.remove());
    this.hide();
    this._toastTimers.forEach(clearTimeout);
    this._toastTimers = [];
  },

  createQuickBuyButton(mint, opts = {}) {
    const btn = document.createElement("button");
    btn.type = "button";
    btn.classList.add(this._cls);
    btn.dataset.mint = mint;
    btn.dataset.buttonType = opts.type || "quick-buy";

    btn.innerHTML = `
      <div style="display:flex;align-items:center;gap:4px;">
        ${SHARK_SVG_14}
        <span style="white-space:nowrap;font-size:13px;font-weight:500;">${this._quickBuyAmount}</span>
      </div>
    `;

    Object.assign(btn.style, {
      border: "1px solid",
      borderRadius: "12px",
      borderColor: this._bgColor,
      color: this._color,
      backgroundColor: this._bgColor,
      cursor: "pointer",
      zIndex: "200",
      fontSize: "12px",
      visibility: "visible",
      transition: "background-color 0.2s ease",
      padding: "0px 8px",
      ...(opts.style || {}),
    });

    const bgColor = this._bgColor;
    btn.onmouseover = function () {
      btn.style.backgroundColor = "rgba(26, 140, 255, 0.5)";
    };
    btn.onmouseout = function () {
      btn.style.backgroundColor = bgColor;
    };

    btn.onclick = async (e) => {
      e.preventDefault();
      e.stopPropagation();

      if (!this._isAuthenticated) {
        this.showToast("Not logged in. Open SlotShark popup to authenticate.", "error");
        return;
      }

      let address = mint;
      if (typeof opts.resolveAddress === "function") {
        address = await opts.resolveAddress();
        if (!address) return;
      }

      this._executeQuickBuy(address, this._quickBuyAmount);
    };

    return btn;
  },

  createPanelButton(mint, opts = {}) {
    const btn = document.createElement("button");
    btn.type = "button";
    btn.classList.add(this._cls + "-p");
    btn.dataset.mint = mint;

    btn.innerHTML = SHARK_SVG_14;

    Object.assign(btn.style, {
      border: "1px solid",
      borderColor: this._bgColor,
      color: this._color,
      backgroundColor: this._bgColor,
      padding: "2px 4px",
      borderRadius: "8px",
      zIndex: "200",
      cursor: "pointer",
      transition: "background-color 0.2s ease",
      display: "inline-flex",
      alignItems: "center",
      justifyContent: "center",
      marginLeft: "4px",
    });

    const bgColor = this._bgColor;
    btn.onmouseover = function () {
      btn.style.backgroundColor = "rgba(26, 140, 255, 0.5)";
    };
    btn.onmouseout = function () {
      btn.style.backgroundColor = bgColor;
    };

    btn.onclick = async (e) => {
      e.preventDefault();
      e.stopPropagation();

      let address = mint;
      if (typeof opts.resolveAddress === "function") {
        address = await opts.resolveAddress();
        if (!address) return;
      }
      this.show(btn, address);
    };

    return btn;
  },

  createInstantTradeButton(label, opts = {}) {
    const btn = document.createElement("button");
    btn.type = "button";
    btn.classList.add(this._cls + "-it");

    btn.innerHTML = `
      <div style="display:flex;align-items:center;justify-content:center;gap:6px;width:100%;">
        ${SHARK_SVG_16}
        <span style="white-space:nowrap;font-size:14px;font-weight:500;">${label}</span>
      </div>
    `;

    Object.assign(btn.style, {
      width: "100%",
      padding: "8px 16px",
      borderRadius: "99px",
      border: `1px solid ${this._bgColor}`,
      backgroundColor: this._bgColor,
      color: this._color,
      cursor: "pointer",
      transition: "background-color 0.2s ease",
    });

    const bgColor = this._bgColor;
    btn.onmouseover = function () {
      btn.style.backgroundColor = "rgba(26, 140, 255, 0.5)";
    };
    btn.onmouseout = function () {
      btn.style.backgroundColor = bgColor;
    };

    btn.onclick = async (e) => {
      e.preventDefault();
      e.stopPropagation();

      if (!this._isAuthenticated) {
        this.showToast("Not logged in. Open SlotShark popup to authenticate.", "error");
        return;
      }

      if (typeof opts.onClick === "function") {
        opts.onClick(e);
      }
    };

    return btn;
  },

  _getWallets() {
    if (this._activeWallets.length > 0) return this._activeWallets;
    if (this._activeWallet) return [this._activeWallet];
    return this._wallets.length > 0 ? [this._wallets[0].pubkey] : [];
  },

  MIN_TRADE_SOL: 0.001,

  _distributeAmount(totalAmount, walletCount) {
    const min = this.MIN_TRADE_SOL;
    if (totalAmount < min) {
      this.showToast(`Minimum trade amount is ${min} SOL`, "error");
      return null;
    }
    if (walletCount <= 1) return [totalAmount];
    const P = 1000; // 3 decimal places (0.001 minimum)
    const dist = this._buyDistribution;
    if (dist === "full") {
      return Array(walletCount).fill(totalAmount);
    } else if (dist === "split") {
      const each = Math.floor((totalAmount / walletCount) * P) / P;
      if (each < min) {
        this.showToast(`Minimum is ${min} SOL per wallet — need at least ${(min * walletCount).toFixed(3)} SOL for ${walletCount} wallets`, "error");
        return null;
      }
      const amounts = Array(walletCount).fill(each);
      const remainder = Math.round((totalAmount - each * walletCount) * P) / P;
      if (remainder > 0) amounts[0] = Math.round((amounts[0] + remainder) * P) / P;
      return amounts;
    } else {
      if (totalAmount < min * walletCount) {
        this.showToast(`Minimum is ${min} SOL per wallet — need at least ${(min * walletCount).toFixed(3)} SOL for ${walletCount} wallets`, "error");
        return null;
      }
      const weights = Array.from({ length: walletCount }, () => 0.4 + Math.random() * 1.2);
      const weightSum = weights.reduce((a, b) => a + b, 0);
      const parts = weights.map((w) => Math.max(min, Math.round((totalAmount * w / weightSum) * P) / P));
      const partsSum = parts.reduce((a, b) => a + b, 0);
      parts[0] = Math.max(min, Math.round((parts[0] + totalAmount - partsSum) * P) / P);
      return parts;
    }
  },

  _isNoTokensError(msg) {
    return typeof msg === "string" && (msg.includes("No token balance") || msg.includes("No tokens to sell"));
  },

  _handleMultiResults(results, action) {
    const ok = results.filter((r) => r?.data?.success).length;
    let failed = results.filter((r) => !r?.data?.success);
    const expired = failed.find((r) => r?.status === 401 || r?.data?.error?.includes("expired"));
    if (expired) {
      this.showToast("Session expired — re-login in the extension popup", "error");
      return;
    }
    // Hide "no tokens owned" failures when at least one wallet succeeded.
    if (ok > 0 && failed.length > 0) {
      failed = failed.filter((r) => !this._isNoTokensError(r?.data?.error || r?.error || ""));
    }
    if (ok > 0) this.showToast(`${action} sent from ${ok} wallet${ok > 1 ? "s" : ""}`, "success");
    if (failed.length > 0) {
      const reason = failed[0]?.data?.error || failed[0]?.error || "unknown error";
      this.showToast(`${failed.length} ${action.toLowerCase()}${failed.length > 1 ? "s" : ""} failed: ${reason}`, "error");
    }

    // On-chain polling — the "sent" toast above only confirms the server
    // accepted the request. The TX may still fail on-chain (pool state change,
    // slippage, migration, etc.). Poll /tx-status/:sig and show a follow-up
    // toast with the actual outcome. Only polls successful sends with a sig.
    const signatures = results
      .filter((r) => r?.data?.success && r?.data?.signature)
      .map((r) => r.data.signature);
    if (signatures.length > 0) this._pollTxOutcomes(signatures, action);
  },

  async _pollTxOutcomes(signatures, action) {
    const outcomes = await Promise.all(signatures.map((sig) => this._pollOneSig(sig)));
    const confirmed = outcomes.filter((o) => o.status === "success").length;
    const failed = outcomes.filter((o) => o.status === "failed");
    const pending = outcomes.filter((o) => o.status === "pending").length;
    const total = signatures.length;
    const single = total === 1;

    if (failed.length > 0) {
      const first = failed[0];
      const reason = first.reason || (first.errorCode != null ? `error ${first.errorCode}` : "reverted");
      const msg = single
        ? `${action} failed: ${reason}`
        : `${action}: ${failed.length}/${total} failed on-chain (${reason})`;
      this.showToast(msg, "error");
    } else if (confirmed === total) {
      const msg = single ? `${action} confirmed on-chain` : `${action}: ${confirmed}/${total} confirmed on-chain`;
      this.showToast(msg, "success");
    } else if (pending > 0) {
      const msg = single
        ? `${action} not yet confirmed — check Solscan`
        : `${action}: ${pending}/${total} not yet confirmed — check Solscan`;
      this.showToast(msg, "error");
    }
  },

  async _pollOneSig(sig) {
    const POLL_INTERVAL_MS = 500;
    const MAX_POLLS = 30; // 15s total
    for (let i = 0; i < MAX_POLLS; i++) {
      try {
        const resp = await this._apiCall("GET", `/tx-status/${sig}`);
        const status = resp?.data?.status;
        if (status === "success" || status === "failed") return resp.data;
      } catch {
        // transient fetch error — keep polling
      }
      await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
    }
    return { status: "pending" };
  },

  async _executeQuickBuy(mint, solAmount) {
    const wallets = this._getWallets();
    if (wallets.length === 0) { this.showToast("No active wallets", "error"); return; }
    const amounts = this._distributeAmount(solAmount, wallets.length);
    if (!amounts) return;

    const calls = wallets.map((wallet, i) => {
      const body = {
        mint,
        solAmount: amounts[i],
        wallet,
        skipIfBought: false,
        slippage: 20,
        antimev: this._quickBuyAntimev,
      };
      if (this._quickBuyTip != null) body.tip = this._quickBuyTip;
      if (this._quickBuyFee != null) body.priorityFee = this._quickBuyFee;
      return { method: "POST", path: "/buy", body };
    });

    if (calls.length === 1) {
      const resp = await this._apiCall(calls[0].method, calls[0].path, calls[0].body);
      this._handleMultiResults([resp], "Buy");
    } else {
      const resp = await this._apiBatch(calls);
      this._handleMultiResults(resp.results || [], "Buy");
    }
  },

  async _executeBuy(mint, solAmount) {
    await this._refreshPreset();
    const v = this._activePresetValues || {};
    const wallets = this._getWallets();
    if (wallets.length === 0) { this.showToast("No active wallets", "error"); return; }
    const amounts = this._distributeAmount(solAmount, wallets.length);
    if (!amounts) return;

    const calls = wallets.map((wallet, i) => {
      const body = {
        mint,
        solAmount: amounts[i],
        wallet,
        skipIfBought: false,
        slippage: v["buy-slippage"] ?? 20,
        antimev: v["buy-anti-mev"] ?? true,
      };
      if (v["buy-tip"] != null) body.tip = v["buy-tip"];
      if (v["buy-fee"] != null) body.priorityFee = v["buy-fee"];
      return { method: "POST", path: "/buy", body };
    });

    if (calls.length === 1) {
      const resp = await this._apiCall(calls[0].method, calls[0].path, calls[0].body);
      this._handleMultiResults([resp], "Buy");
    } else {
      const resp = await this._apiBatch(calls);
      this._handleMultiResults(resp.results || [], "Buy");
    }
  },

  async _executeSell(mint, sellPercent) {
    await this._refreshPreset();
    const v = this._activePresetValues || {};
    const wallets = this._getWallets();
    if (wallets.length === 0) { this.showToast("No active wallets", "error"); return; }

    const calls = wallets.map((wallet) => {
      const body = {
        mint,
        sellPercent,
        wallet,
        slippage: v["sell-slippage"] ?? 20,
        antimev: v["sell-anti-mev"] ?? true,
      };
      if (v["sell-tip"] != null) body.tip = v["sell-tip"];
      if (v["sell-fee"] != null) body.priorityFee = v["sell-fee"];
      return { method: "POST", path: "/sell", body };
    });

    if (calls.length === 1) {
      const resp = await this._apiCall(calls[0].method, calls[0].path, calls[0].body);
      this._handleMultiResults([resp], "Sell");
    } else {
      const resp = await this._apiBatch(calls);
      this._handleMultiResults(resp.results || [], "Sell");
    }
  },

  async _refreshPreset() {
    try {
      const data = await chrome.storage.local.get([
        "slotshark.presets", "slotshark.activePreset",
        "slotshark.activeWallets", "slotshark.buyDistribution",
      ]);
      const presets = data["slotshark.presets"] || [];
      const activeId = data["slotshark.activePreset"];
      const active = presets.find((p) => p.id === activeId) || presets[0];
      if (active && active.values) {
        this._activePresetValues = active.values;
        this._buyPresets = active.values["buy-amounts"] || [0.5, 1, 2, 5];
        this._sellPresets = active.values["sell-percents"] || [10, 25, 50, 100];
      }
      this._activeWallets = data["slotshark.activeWallets"] || [];
      this._buyDistribution = data["slotshark.buyDistribution"] || "full";
    } catch { /* storage unavailable */ }
  },

  async show(anchorEl, mint) {
    this.hide();
    await this._refreshPreset();

    const panel = document.createElement("div");
    panel.className = "ss-panel";
    this._panelEl = panel;

    const header = document.createElement("div");
    header.className = "ss-panel-header";
    header.innerHTML = `
      <div class="ss-panel-title">${SHARK_SVG_14}SlotShark</div>
      <button class="ss-panel-close">&times;</button>
    `;
    header.querySelector(".ss-panel-close").addEventListener("click", () => this.hide());
    panel.appendChild(header);

    const body = document.createElement("div");
    body.className = "ss-panel-body";

    if (!this._isAuthenticated) {
      body.innerHTML = '<div class="ss-auth-msg">Not logged in. Open the SlotShark popup to authenticate.</div>';
      panel.appendChild(body);
      this._mount(panel, anchorEl);
      return;
    }

    const mintDisplay = document.createElement("div");
    mintDisplay.className = "ss-mint-display";
    mintDisplay.textContent = mint.slice(0, 8) + "..." + mint.slice(-6);
    mintDisplay.title = mint;
    body.appendChild(mintDisplay);

    if (this._wallets.length > 0) {
      const walletSection = document.createElement("div");
      walletSection.className = "ss-wallet-section";
      const walletLabel = document.createElement("div");
      walletLabel.className = "ss-wallet-label";
      walletLabel.textContent = "Wallet";
      walletSection.appendChild(walletLabel);
      const select = document.createElement("select");
      select.className = "ss-wallet-select";
      for (const w of this._wallets) {
        const opt = document.createElement("option");
        opt.value = w.pubkey;
        opt.textContent = w.label || (w.pubkey.slice(0, 6) + "..." + w.pubkey.slice(-4));
        if (w.pubkey === this._activeWallet) opt.selected = true;
        select.appendChild(opt);
      }
      select.addEventListener("change", () => {
        this._activeWallet = select.value;
        chrome.storage.local.set({ "slotshark.activeWallet": select.value }).catch(() => {});
      });
      walletSection.appendChild(select);
      body.appendChild(walletSection);
    }

    const buyLabel = document.createElement("div");
    buyLabel.className = "ss-section-label";
    buyLabel.textContent = "Buy";
    body.appendChild(buyLabel);
    const buyGrid = document.createElement("div");
    buyGrid.className = "ss-presets-grid";
    for (const amount of this._buyPresets) {
      const btn = document.createElement("button");
      btn.className = "ss-preset-btn";
      btn.textContent = `${amount} SOL`;
      btn.addEventListener("click", () => {
        btn.classList.add("ss-loading");
        this._executeBuy(mint, amount).then(() => {
          btn.classList.remove("ss-loading");
        });
      });
      buyGrid.appendChild(btn);
    }
    body.appendChild(buyGrid);

    const sellLabel = document.createElement("div");
    sellLabel.className = "ss-section-label";
    sellLabel.textContent = "Sell";
    body.appendChild(sellLabel);
    const sellGrid = document.createElement("div");
    sellGrid.className = "ss-presets-grid";
    for (const pct of this._sellPresets) {
      const btn = document.createElement("button");
      btn.className = "ss-preset-btn ss-sell";
      btn.textContent = `${pct}%`;
      btn.addEventListener("click", () => {
        btn.classList.add("ss-loading");
        this._executeSell(mint, pct).then(() => {
          btn.classList.remove("ss-loading");
        });
      });
      sellGrid.appendChild(btn);
    }
    body.appendChild(sellGrid);

    panel.appendChild(body);
    this._mount(panel, anchorEl);
  },

  _mount(panel, anchorEl) {
    document.body.appendChild(panel);

    const rect = anchorEl.getBoundingClientRect();
    const pw = 280;
    const ph = panel.offsetHeight;

    let left = rect.right + 8;
    let top = rect.top + (rect.height / 2) - (ph / 2);

    if (left + pw > window.innerWidth - 8) left = rect.left - pw - 8;
    if (top < 8) top = 8;
    if (top + ph > window.innerHeight - 8) top = window.innerHeight - ph - 8;
    if (left < 8) left = 8;

    panel.style.left = left + "px";
    panel.style.top = top + "px";

    if (this._mountTimeoutId) {
      clearTimeout(this._mountTimeoutId);
      this._mountTimeoutId = null;
    }

    this._dismissHandler = (e) => {
      if (!panel.contains(e.target) && !e.target.closest(`[class*="${this._cls}"]`)) {
        this.hide();
      }
    };
    this._escHandler = (e) => {
      if (e.key === "Escape") this.hide();
    };

    this._mountTimeoutId = setTimeout(() => {
      this._mountTimeoutId = null;
      document.addEventListener("click", this._dismissHandler, true);
      document.addEventListener("keydown", this._escHandler, true);
    }, 50);
  },

  hide() {
    if (this._mountTimeoutId) {
      clearTimeout(this._mountTimeoutId);
      this._mountTimeoutId = null;
    }
    if (this._panelEl) {
      this._panelEl.remove();
      this._panelEl = null;
    }
    if (this._dismissHandler) {
      document.removeEventListener("click", this._dismissHandler, true);
      this._dismissHandler = null;
    }
    if (this._escHandler) {
      document.removeEventListener("keydown", this._escHandler, true);
      this._escHandler = null;
    }
  },

  showToast(message, type = "success", durationMs = null) {
    // Stacked toasts: find or create a single container, append each toast as
    // a flex-column child. Newest goes to the bottom; as older ones dismiss
    // they're removed from the DOM and the remaining stack shifts up.
    let container = document.getElementById("ss-toast-container");
    if (!container) {
      container = document.createElement("div");
      container.id = "ss-toast-container";
      container.className = "ss-toast-container";
      document.body.appendChild(container);
    }

    const toast = document.createElement("div");
    toast.className = `ss-toast ss-toast-${type}`;
    toast.textContent = message;
    container.appendChild(toast);
    requestAnimationFrame(() => toast.classList.add("ss-show"));

    const dismissAfter = durationMs != null ? durationMs : (type === "error" ? 4000 : 1500);
    const t1 = setTimeout(() => {
      toast.classList.remove("ss-show");
      const t2 = setTimeout(() => {
        toast.remove();
        this._toastTimers = this._toastTimers.filter((t) => t !== t1 && t !== t2);
      }, 150);
      this._toastTimers.push(t2);
    }, dismissAfter);
    this._toastTimers.push(t1);
  },
};
