/* 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "serviceMode",
  "cookiebanners.service.mode",
  Ci.nsICookieBannerService.MODE_DISABLED
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "serviceModePBM",
  "cookiebanners.service.mode.privateBrowsing",
  Ci.nsICookieBannerService.MODE_DISABLED
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "maxTriesPerSiteAndSession",
  "cookiebanners.bannerClicking.maxTriesPerSiteAndSession",
  3
);

ChromeUtils.defineLazyGetter(lazy, "CookieBannerL10n", () => {
  return new Localization([
    "branding/brand.ftl",
    "toolkit/global/cookieBannerHandling.ftl",
  ]);
});

export class CookieBannerParent extends JSWindowActorParent {
  /**
   * Get the browser associated with this window which is the top level embedder
   * element. Returns null if the top embedder isn't a browser.
   */
  get #browserElement() {
    let topBC = this.browsingContext.top;

    // Not all embedders are browsers.
    if (topBC.embedderElementType != "browser") {
      return null;
    }

    return topBC.embedderElement;
  }

  get #isTopLevel() {
    return !this.manager.browsingContext.parent;
  }

  #isPrivateBrowsingCached;
  get #isPrivateBrowsing() {
    if (this.#isPrivateBrowsingCached !== undefined) {
      return this.#isPrivateBrowsingCached;
    }
    let browser = this.#browserElement;
    if (!browser) {
      return false;
    }

    this.#isPrivateBrowsingCached =
      lazy.PrivateBrowsingUtils.isBrowserPrivate(browser);

    return this.#isPrivateBrowsingCached;
  }

  /**
   * Dispatches a custom "cookiebannerhandled" event on the chrome window.
   */
  #notifyCookieBannerState(eventType) {
    let chromeWin = this.browsingContext.topChromeWindow;
    if (!chromeWin) {
      return;
    }
    let windowUtils = chromeWin.windowUtils;
    if (!windowUtils) {
      return;
    }
    let event = new CustomEvent(eventType, {
      bubbles: true,
      cancelable: false,
      detail: {
        windowContext: this.manager,
      },
    });
    windowUtils.dispatchEventToChromeOnly(chromeWin, event);
  }

  /**
   * Logs a warning to the web console that a cookie banner has been handled.
   */
  async #logCookieBannerHandledToWebConsole() {
    let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
      Ci.nsIScriptError
    );
    let [message] = await lazy.CookieBannerL10n.formatMessages([
      { id: "cookie-banner-handled-webconsole" },
    ]);

    if (!this.manager?.innerWindowId) {
      return;
    }
    consoleMsg.initWithWindowID(
      message.value,
      this.manager.documentURI?.spec,
      0,
      0,
      Ci.nsIScriptError.warningFlag,
      "cookiebannerhandling",
      this.manager?.innerWindowId
    );
    Services.console.logMessage(consoleMsg);
  }

  async receiveMessage(message) {
    if (message.name == "CookieBanner::Test-FinishClicking") {
      Services.obs.notifyObservers(
        null,
        "cookie-banner-test-clicking-finish",
        this.manager.documentPrincipal?.baseDomain
      );
      return undefined;
    }

    // Forwards cookie banner detected signals to frontend consumers.
    if (message.name == "CookieBanner::DetectedBanner") {
      this.#notifyCookieBannerState("cookiebannerdetected");
      return undefined;
    }

    // Forwards cookie banner handled signals to frontend consumers.
    if (message.name == "CookieBanner::HandledBanner") {
      this.#notifyCookieBannerState("cookiebannerhandled");
      this.#logCookieBannerHandledToWebConsole();
      return undefined;
    }

    let domain = this.manager.documentPrincipal?.baseDomain;

    if (message.name == "CookieBanner::MarkSiteExecuted") {
      if (!domain) {
        return undefined;
      }

      Services.cookieBanners.markSiteExecuted(
        domain,
        this.#isTopLevel,
        this.#isPrivateBrowsing
      );
      return undefined;
    }

    if (message.name != "CookieBanner::GetClickRules") {
      return undefined;
    }

    // TODO: Bug 1790688: consider moving this logic to the cookie banner service.
    let mode;
    if (this.#isPrivateBrowsing) {
      mode = lazy.serviceModePBM;
    } else {
      mode = lazy.serviceMode;
    }

    // Check if we have a site preference of the top-level URI. If so, it
    // takes precedence over the pref setting.
    let topBrowsingContext = this.manager.browsingContext.top;
    let topURI = topBrowsingContext.currentWindowGlobal?.documentURI;

    // We don't need to check the domain preference if the cookie banner
    // handling was disabled by pref.
    if (mode != Ci.nsICookieBannerService.MODE_DISABLED && topURI) {
      try {
        let perDomainMode = Services.cookieBanners.getDomainPref(
          topURI,
          this.#isPrivateBrowsing
        );

        if (perDomainMode != Ci.nsICookieBannerService.MODE_UNSET) {
          mode = perDomainMode;
        }
      } catch (e) {
        // getPerSitePref could throw with NS_ERROR_NOT_AVAILABLE if the service
        // is disabled. We will fallback to global pref setting if any errors
        // occur.
        if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
          console.error("The cookie banner handling service is not available");
        } else {
          console.error("Fail on getting domain pref:", e);
        }
      }
    }

    // Check if we previously executed banner clicking for the site. If the pref
    // instructs to always execute banner clicking, we will set it to
    // false.
    let hasExecuted = false;
    if (lazy.maxTriesPerSiteAndSession > 0) {
      hasExecuted = Services.cookieBanners.shouldStopBannerClickingForSite(
        domain,
        this.#isTopLevel,
        this.#isPrivateBrowsing
      );
    }

    // If we have previously executed banner clicking or the service is disabled
    // for current context (normal or private browsing), return empty array.
    if (hasExecuted || mode == Ci.nsICookieBannerService.MODE_DISABLED) {
      return { rules: [], hasExecuted };
    }

    if (!domain) {
      return { rules: [], hasExecuted };
    }

    let rules = Services.cookieBanners.getClickRulesForDomain(
      domain,
      this.#isTopLevel
    );

    if (!rules.length) {
      return { rules: [], hasExecuted };
    }

    // Determine whether we can fall back to opt-in rules. This includes the
    // detect-only mode where don't interact with the banner.
    let modeAllowsOptIn =
      mode == Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT;

    rules = rules.map(rule => {
      let target = rule.optOut;

      if (modeAllowsOptIn && !target) {
        target = rule.optIn;
      }
      return {
        hide: rule.hide ?? rule.presence,
        presence: rule.presence,
        skipPresenceVisibilityCheck: rule.skipPresenceVisibilityCheck,
        target,
      };
    });

    return { rules, hasExecuted };
  }
}
