/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  PopupNotifications: "resource://gre/modules/PopupNotifications.sys.mjs",
  SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
});

const { SidebarPermissionsUI } = ChromeUtils.importESModule(
  "chrome://browser/content/sidebar/sidebar-permissions-ui.mjs"
);

/**
 * Get count of queued tab notifications
 */
function getTabNotificationCount(win) {
  const tabBrowser = win.gBrowser?.selectedBrowser;
  if (!tabBrowser || !win.PopupNotifications) {
    return 0;
  }

  const notifications =
    win.PopupNotifications.getNotificationsForBrowser(tabBrowser);
  return notifications.length ?? 0;
}

/**
 * Cancel all tab notifications within same window.
 * When sidebar takes over, we cancel all pending tab notifications,
 * not just the one matching the current permission type.
 */
function cancelAllTabNotifications(win) {
  const tabBrowser = win.gBrowser?.selectedBrowser;

  if (tabBrowser && win.PopupNotifications) {
    removeNotificationsForBrowser(win.PopupNotifications, tabBrowser);
  }
}

/**
 * Remove all pending notifications for a browser from a notification system.
 *
 * @param {PopupNotifications} popupNotifications - The notification system (SidebarPopupNotifications)
 * @param {Browser} browser - The browser to remove notifications
 */
function removeNotificationsForBrowser(popupNotifications, browser) {
  const notifications = popupNotifications.getNotificationsForBrowser(browser);
  if (notifications?.length) {
    for (const notification of [...notifications]) {
      popupNotifications.remove(notification, true);
    }
  }
}

/**
 * Per window Sidebar Permissions handles:
 * - webrtc request UI
 * - webrtc sharing UI
 * - blocked webrtc UI
 * - sidebar PopupNotification placement
 * - PopupNotification coordination between sidebar and tabs (same window and cross window)
 */
export class SidebarPermissions {
  #initialized = false;
  #window;
  #browser = null;
  #contentBrowser = null;
  #sidebarPermissionUI = null;
  #observerBound = false;
  #popupshowingHandler;
  #panelOpenHandler;
  #popuphiddenHandler;
  #currentPopupNotificationBrowser;
  #onPermissionStateChanged;
  #onSidebarBrowserChanged;
  #onSidebarHideEvent;
  #securityChangeListener;

  constructor(win) {
    this.#window = win;
  }

  /**
   * Getter for contentBrowser
   */
  get contentBrowser() {
    return this.#contentBrowser;
  }

  /**
   * Called by SidebarController after the sidebar nested browser is ready.
   * Sets up all the permission UI and observers.
   */
  init(browser) {
    const win = this.#window;
    this.#browser = browser;

    if (!this.#browser?.contentDocument) {
      console.warn("Sidebar browser is not ready");
      return;
    }

    // Window level setup - per window
    if (!this.#initialized) {
      this.#bindObservers();

      // Also listen to custom event as a hint, but verify with actual DOM check
      // this allows chat.js or other tools to trigger changes
      this.#onSidebarBrowserChanged = this.onContentBrowserChanged.bind(this);
      win.addEventListener(
        "sidebarbrowserchanged",
        this.#onSidebarBrowserChanged
      );

      win.addEventListener("unload", () => this.#uninit(), {
        once: true,
      });

      this.#initialized = true;
    }

    // Ensure SidebarPopupNotifications exists (may need to recreate after sidebar close/reopen)
    if (!win.SidebarPopupNotifications) {
      this.#setupSidebarPopupNotifications();
    }

    // Content level setup - runs every eligible permission sidebar tool opens
    // When sidebar is closed or change tool
    this.#sidebarPermissionUI = new SidebarPermissionsUI(win, this.#browser);
    this.#sidebarPermissionUI.build();
    this.#setupSidebarPopupNotificationWrapper();

    this.#onSidebarHideEvent = () => this.onSidebarHidden();
    this.#browser.contentWindow?.addEventListener(
      "SidebarWillHide",
      this.#onSidebarHideEvent
    );

    if (this.#contentBrowser && this.#onPermissionStateChanged) {
      this.#contentBrowser.removeEventListener(
        "PermissionStateChange",
        this.#onPermissionStateChanged,
        true
      );
    }

    this.#contentBrowser =
      this.#browser.contentDocument.querySelector("browser");
    this.#sidebarPermissionUI.setContentBrowser(this.#contentBrowser);

    this.#setupPermissionStateListener();
  }

  /**
   * Remove SidebarPopupNotification.show wrappers when sidebar is hidden
   */
  onSidebarHidden() {
    const win = this.#window;

    if (this.#observerBound) {
      Services.obs.removeObserver(this, "perm-changed");
      this.#observerBound = false;
    }

    if (this.#contentBrowser?._sharingState) {
      this.#contentBrowser._sharingState = {};
    }

    // Cancel any active SidebarPopupNotification before unwrapping
    this.#cancelSidebarNotifications();

    const panel = win.document?.getElementById("notification-popup");
    if (this.#popupshowingHandler) {
      panel?.removeEventListener(
        "popupshowing",
        this.#popupshowingHandler,
        true
      );
      this.#popupshowingHandler = null;
    }
    if (this.#panelOpenHandler) {
      panel?.removeEventListener("popupshown", this.#panelOpenHandler, true);
      panel?.removeEventListener("PanelUpdated", this.#panelOpenHandler, true);
      this.#panelOpenHandler = null;
    }
    if (this.#popuphiddenHandler) {
      panel?.removeEventListener("popuphidden", this.#popuphiddenHandler, true);
      this.#popuphiddenHandler = null;
    }
    this.#currentPopupNotificationBrowser = null;

    if (this.#onSidebarBrowserChanged) {
      win.removeEventListener(
        "sidebarbrowserchanged",
        this.#onSidebarBrowserChanged
      );
      this.#onSidebarBrowserChanged = null;
    }

    // Remove wrapper when sidebar is closed/hidden
    if (win.SidebarPopupNotifications) {
      win.SidebarPopupNotifications._currentAnchorElement = null;

      if (win.SidebarPopupNotifications._wrappedBySidebarPermissions) {
        win.SidebarPopupNotifications.show =
          win.SidebarPopupNotifications._originalShow;
      }

      delete win.SidebarPopupNotifications;
    }

    if (this.#contentBrowser && this.#securityChangeListener) {
      try {
        this.#contentBrowser.removeProgressListener(
          this.#securityChangeListener
        );
      } catch (e) {}
      this.#securityChangeListener = null;
    }

    if (this.#contentBrowser && this.#onPermissionStateChanged) {
      this.#contentBrowser.removeEventListener(
        "PermissionStateChange",
        this.#onPermissionStateChanged,
        true
      );
      this.#onPermissionStateChanged = null;
    }

    this.#browser?.contentWindow?.removeEventListener(
      "SidebarWillHide",
      this.#onSidebarHideEvent
    );
    this.#onSidebarHideEvent = null;

    // Clear references to prevent leaks
    this.#browser = null;
    this.#contentBrowser = null;
    this.#sidebarPermissionUI?.destroy();
    this.#sidebarPermissionUI = null;
  }

  /**
   * Clean up observers and listeners on unload.
   */
  #uninit() {
    const win = this.#window;

    if (this.#contentBrowser && this.#securityChangeListener) {
      try {
        this.#contentBrowser.removeProgressListener(
          this.#securityChangeListener
        );
      } catch (e) {
        // Listener wasn't on this browser
        console.warn("Failed to remove progress listener:", e.message);
      }
      this.#securityChangeListener = null;
    }

    if (this.#contentBrowser && this.#onPermissionStateChanged) {
      this.#contentBrowser.removeEventListener(
        "PermissionStateChange",
        this.#onPermissionStateChanged,
        true
      );
      this.#onPermissionStateChanged = null;
    }

    if (this.#browser?.contentWindow && this.#onSidebarHideEvent) {
      this.#browser.contentWindow.removeEventListener(
        "SidebarWillHide",
        this.#onSidebarHideEvent
      );
      this.#onSidebarHideEvent = null;
    }

    // This makes sure the wrappers are removed even if hide event didn't fire
    this.onSidebarHidden();

    const panel = win.document?.getElementById("notification-popup");
    if (this.#popupshowingHandler) {
      panel?.removeEventListener(
        "popupshowing",
        this.#popupshowingHandler,
        true
      );
      this.#popupshowingHandler = null;
    }

    if (this.#panelOpenHandler) {
      panel?.removeEventListener("popupshown", this.#panelOpenHandler, true);

      panel?.removeEventListener("PanelUpdated", this.#panelOpenHandler, true);
      this.#panelOpenHandler = null;
    }

    if (this.#popuphiddenHandler) {
      panel?.removeEventListener("popuphidden", this.#popuphiddenHandler, true);
      this.#popuphiddenHandler = null;
    }
    this.#currentPopupNotificationBrowser = null;

    if (this.#observerBound) {
      Services.obs.removeObserver(this, "perm-changed");
      this.#observerBound = false;
    }

    if (this.#onSidebarBrowserChanged) {
      this.#window.removeEventListener(
        "sidebarbrowserchanged",
        this.#onSidebarBrowserChanged
      );
      this.#onSidebarBrowserChanged = null;
    }

    if (win.SidebarPopupNotifications) {
      delete win.SidebarPopupNotifications;
    }

    // Reset browser override
    if (win.gPermissionPanel) {
      win.gPermissionPanel.clearBrowserOverride();
    }

    this.#sidebarPermissionUI?.destroy();
    this.#sidebarPermissionUI = null;
    this.#contentBrowser = null;
    this.#browser = null;
  }

  /**
   * Create SidebarPopupNotifications instance for this window.
   * This redirects PopupNotification to use sidebar anchors instead of browser chrome.
   * Because both sidebar and tab PopupNotifications use the same panel element
   * this function wraps SidebarPopupNotification.show() methods to cancel the others before showing to ensure only
   * one permission popup is visible at a time.
   */
  #setupSidebarPopupNotifications() {
    const win = this.#window;

    const chromeDoc = win.document;
    let panel = chromeDoc.getElementById("notification-popup");
    const iconBox = chromeDoc.getElementById("notification-popup-box");

    win.SidebarPopupNotifications = new lazy.PopupNotifications(
      this.#browser,
      panel,
      iconBox,
      {
        getVisibleAnchorElement(anchorElement) {
          if (anchorElement?.checkVisibility()) {
            return anchorElement;
          }

          const sidebarDocument =
            win.SidebarController?.browser?.contentDocument;
          const micAnchor = sidebarDocument?.getElementById(
            "sidebar-webrtc-microphone-notification-icon"
          );
          if (micAnchor?.checkVisibility()) {
            return micAnchor;
          }

          return iconBox;
        },
      }
    );

    // Only  when a sidebar is open, hook SidebarPopupNotification show()
    // Cancel pending all other PopupNotifications
    this.#wrapSidebarShow(win.SidebarPopupNotifications);

    // Track which browser notification is showing
    this.#currentPopupNotificationBrowser = null;

    this.#popupshowingHandler = () => {
      const firstChild = panel.firstElementChild;
      const browser = firstChild?.notification?.browser;
      const isSidebarNotification = browser === this.#contentBrowser;

      if (isSidebarNotification) {
        this.#currentPopupNotificationBrowser = browser;
        this.#sidebarPermissionUI.showMicRequestUI();
      }
    };

    this.#panelOpenHandler = () => {
      this.#handlePopupChange(panel);
    };

    this.#popuphiddenHandler = () => {
      // Ignore if we're in the middle of showing sidebar
      if (win.SidebarPopupNotifications?._isShowing) {
        return;
      }
      // Only handle sidebar PopupNotifications
      if (this.#currentPopupNotificationBrowser === this.#contentBrowser) {
        this.#onSidebarPopupNotificationHidden();
      }
    };

    // Store current PopupNotification Browser to check with this.#contentBrowser.
    panel.addEventListener("popupshowing", this.#popupshowingHandler, true);
    panel.addEventListener("popupshown", this.#panelOpenHandler, true);
    panel.addEventListener("PanelUpdated", this.#panelOpenHandler, true);
    // When the active SidebarPopupNotification is hidden just cancel the notification
    panel.addEventListener("popuphidden", this.#popuphiddenHandler, true);
  }

  #handlePopupChange(panel) {
    if (panel.state !== "open") {
      return;
    }

    const firstChild = panel.firstElementChild;
    const browser = firstChild?.notification?.browser;
    this.#currentPopupNotificationBrowser = browser;

    const isSidebarNotification = browser === this.#contentBrowser;

    if (!isSidebarNotification) {
      this.#cancelSidebarNotifications();
    }
  }

  /**
   * Setup SidebarPopupNotification wrapper for sidebar.
   * Called on every sidebar open or changed provider
   */
  #setupSidebarPopupNotificationWrapper() {
    const win = this.#window;

    if (win.SidebarPopupNotifications) {
      // if it is wrapped already it won't be rewrapped
      this.#wrapSidebarShow(win.SidebarPopupNotifications);
    }
  }

  /**
   * Called when sidebar PopupNotification is hidden
   */
  #onSidebarPopupNotificationHidden() {
    this.#cancelSidebarNotifications();
    this.updatePermissionIcons();
  }

  /**
   * Cancel all SidebarPopupNotification in current window.
   */
  #cancelSidebarNotifications() {
    const win = this.#window;

    if (this.#contentBrowser && win.SidebarPopupNotifications) {
      removeNotificationsForBrowser(
        win.SidebarPopupNotifications,
        this.#contentBrowser
      );
    }
  }

  /**
   * Wrap SidebarPopupNotifications.show() to cancel tab notifications
   * if SidebarPopupNotification is requested.
   *
   * @param {PopupNotifications} sidebarInstance - The SidebarPopupNotifications instance
   */
  #wrapSidebarShow(sidebarInstance) {
    // TODO - Bug 2009301: This approach is hacky to cancel the active tab Notification.
    //  Should revisit and find a better solution.
    if (sidebarInstance._wrappedBySidebarPermissions) {
      return;
    }

    const originalShow = sidebarInstance.show;
    const win = this.#window;
    sidebarInstance._isShowing = false;

    sidebarInstance.show = function (browser, id, ...args) {
      const panel = win.document.getElementById("notification-popup");

      sidebarInstance._isShowing = true;

      // Check how many tab notifications exist
      const tabNotificationCount = getTabNotificationCount(win);
      if (tabNotificationCount >= 1) {
        // Cancel all tab notifications
        cancelAllTabNotifications(win);
      }

      // When multiple tab notifications are queued, canceling them triggers
      // multiple popuphidden events. wait for the panels to fully closed before
      // showing the sidebar notification to avoid UI conflicts.
      if (tabNotificationCount > 1 && panel) {
        const showAfterHidden = async () => {
          await new Promise(resolve => {
            if (panel && panel.state === "closed") {
              panel.addEventListener("popuphidden", resolve, { once: true });
            } else {
              resolve();
            }
          });

          const result = originalShow.call(
            sidebarInstance,
            browser,
            id,
            ...args
          );
          sidebarInstance._isShowing = false;
          return result;
        };
        return showAfterHidden();
      }

      // No queued notifications in tab, show SidebarNotification immediately.
      const result = originalShow.call(sidebarInstance, browser, id, ...args);
      sidebarInstance._isShowing = false;
      return result;
    };

    sidebarInstance._wrappedBySidebarPermissions = true;
    sidebarInstance._originalShow = originalShow;
  }

  #bindObservers() {
    if (this.#observerBound) {
      return;
    }

    Services.obs.addObserver(this, "perm-changed");
    this.#observerBound = true;
  }

  observe(subject, topic) {
    switch (topic) {
      case "perm-changed":
        this.#onPermissionChanged(subject);
        break;
    }
  }

  onContentBrowserChanged() {
    const win = this.#window;
    // Remove listeners from old browser
    if (this.#contentBrowser && this.#onPermissionStateChanged) {
      this.#contentBrowser.removeEventListener(
        "PermissionStateChange",
        this.#onPermissionStateChanged,
        true
      );
    }

    if (this.#contentBrowser && this.#securityChangeListener) {
      try {
        this.#contentBrowser.removeProgressListener(
          this.#securityChangeListener
        );
      } catch (e) {
        // Listener wasn't on this browser
        console.warn("Failed to remove progress listener:", e.message);
      }
    }

    // Cancel any active sidebar notification for older browser
    this.#cancelSidebarNotifications();

    // Reset UI
    this.#sidebarPermissionUI?.clearUI();

    // Get the new provider browser
    this.#contentBrowser =
      this.#browser?.contentDocument?.querySelector("browser");
    this.#sidebarPermissionUI.setContentBrowser(this.#contentBrowser);

    if (this.#contentBrowser) {
      const _self = this;

      this.#securityChangeListener = {
        _previousOrigin: null,

        onSecurityChange() {
          const currentOrigin = _self.#contentBrowser?.contentPrincipal?.origin;

          if (this._previousOrigin === currentOrigin) {
            return;
          }

          this._previousOrigin = currentOrigin;
          _self.updatePermissionIcons();
        },
        QueryInterface: ChromeUtils.generateQI([
          "nsIWebProgressListener",
          "nsISupportsWeakReference",
        ]),
      };

      this.#contentBrowser.addProgressListener(
        this.#securityChangeListener,
        Ci.nsIWebProgress.NOTIFY_SECURITY
      );

      this.#setupPermissionStateListener();
    }

    // Reset browser override to gPanel
    if (win.gPermissionPanel) {
      win.gPermissionPanel.clearBrowserOverride();
    }
  }

  #onPermissionChanged(subject) {
    // Only care about microphone for now
    const permission = subject.QueryInterface(Ci.nsIPermission);
    if (permission.type !== "microphone") {
      return;
    }

    if (!this.#sidebarPermissionUI?.isReady()) {
      return;
    }

    if (this.#sidebarPermissionUI.isIdentityBoxOpen()) {
      return;
    }

    const sidebarPrincipal = this.#contentBrowser?.contentPrincipal;

    // Only care if permission is the same as sidebar's origin
    if (permission.principal?.origin !== sidebarPrincipal?.origin) {
      return;
    }

    this.updatePermissionIcons();
  }

  #setupPermissionStateListener() {
    if (!this.#contentBrowser) {
      return;
    }

    this.#onPermissionStateChanged = event => {
      const currentBrowser = event.target;

      // Sidebar browser changed
      if (currentBrowser === this.#contentBrowser) {
        this.updatePermissionIcons();
      }
    };

    // Listen on contentBrowser for sidebar permission changes
    this.#contentBrowser.addEventListener(
      "PermissionStateChange",
      this.#onPermissionStateChanged,
      true
    );
  }

  /**
   * Update permission icons based on current permission state.
   * Supporting only microphone for now
   */
  updatePermissionIcons() {
    if (!this.#contentBrowser) {
      return;
    }

    let permissions = lazy.SitePermissions.getAllForBrowser(
      this.#contentBrowser
    );

    // If no permissions, clear UI
    if (!permissions.length) {
      this.#sidebarPermissionUI.clearUI();
      return;
    }

    // Filter permission matching type (e.g. "microphone" or "microphone^DeviceName")
    const matchingMicPermissions = permissions.filter(
      p => p.id === "microphone" || p.id.startsWith("microphone^")
    );

    // Check if any matching permission is blocked
    if (
      matchingMicPermissions.some(p => p.state === lazy.SitePermissions.BLOCK)
    ) {
      this.#sidebarPermissionUI.showBlockedUI(true);
      return;
    }

    // Check if any matching permission is allowed
    if (
      matchingMicPermissions.some(p => p.state === lazy.SitePermissions.ALLOW)
    ) {
      this.#sidebarPermissionUI.showGrantedUI("microphone");
      return;
    }

    this.#sidebarPermissionUI.clearUI();
  }

  showMicRequestUI() {
    this.#sidebarPermissionUI.showMicRequestUI();
  }

  /**
   * Show the granted permission UI indicator.
   *
   * @param {string} type - Permission type (e.g. "microphone")
   */
  showGrantedUI(type) {
    this.#sidebarPermissionUI.showGrantedUI(type);
  }

  /**
   * Update sharing icon separately from browser-sitePermissionPanel.
   *
   * @param {object} sharingState - Browser sharing state object
   */
  updateFromBrowserState(sharingState) {
    this.#sidebarPermissionUI?.updateFromBrowserState(sharingState);
  }
}
