/* 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 { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";

const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPush");

const lazy = {};

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

// Observer notification topics for push messages and subscription status
// changes. These are duplicated and used in `nsIPushNotifier`. They're exposed
// on `nsIPushService` so that JS callers only need to import this service.
const OBSERVER_TOPIC_PUSH = "push-message";
const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified";

function createSubscription({
  scope,
  browserPublicKey,
  authSecret,
  endpoint,
  appServerKey,
}) {
  const decodedBrowserKey = ChromeUtils.base64URLDecode(browserPublicKey, {
    padding: "ignore",
  });
  const decodedAuthSecret = ChromeUtils.base64URLDecode(authSecret, {
    padding: "ignore",
  });

  return new PushSubscription({
    endpoint,
    scope,
    p256dhKey: decodedBrowserKey,
    authenticationSecret: decodedAuthSecret,
    appServerKey,
  });
}

function scopeWithAttrs(scope, attrs) {
  return scope + ChromeUtils.originAttributesToSuffix(attrs);
}

export class PushService {
  constructor() {
    this.wrappedJSObject = this;
  }

  pushTopic = OBSERVER_TOPIC_PUSH;
  subscriptionChangeTopic = OBSERVER_TOPIC_SUBSCRIPTION_CHANGE;
  subscriptionModifiedTopic = OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED;

  // nsIObserver methods

  observe() {}

  // nsIPushService methods

  subscribe(scope, principal, callback) {
    this.subscribeWithKey(scope, principal, null, callback);
  }

  async subscribeWithKey(scope, principal, appServerKey, callback) {
    const keyView = new Uint8Array(appServerKey);

    if (appServerKey != null) {
      try {
        await lazy.PushCrypto.validateAppServerKey(keyView);
      } catch (error) {
        callback.onPushSubscription(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR, null);
        return;
      }
    }

    try {
      const response = await lazy.EventDispatcher.instance.sendRequestForResult(
        {
          type: "GeckoView:PushSubscribe",
          scope: scopeWithAttrs(scope, principal.originAttributes),
          appServerKey: appServerKey
            ? ChromeUtils.base64URLEncode(keyView, {
                pad: true,
              })
            : null,
        }
      );

      let subscription = null;
      if (response) {
        subscription = createSubscription({
          ...response,
          scope,
          principal,
          appServerKey,
        });
      }

      callback.onPushSubscription(Cr.NS_OK, subscription);
    } catch (e) {
      callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null);
    }
  }

  async unsubscribe(scope, principal, callback) {
    try {
      await lazy.EventDispatcher.instance.sendRequestForResult({
        type: "GeckoView:PushUnsubscribe",
        scope: scopeWithAttrs(scope, principal.originAttributes),
      });

      callback.onUnsubscribe(Cr.NS_OK, true);
    } catch (e) {
      callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
    }
  }

  async getSubscription(scope, principal, callback) {
    try {
      const response = await lazy.EventDispatcher.instance.sendRequestForResult(
        {
          type: "GeckoView:PushGetSubscription",
          scope: scopeWithAttrs(scope, principal.originAttributes),
        }
      );

      let subscription = null;
      if (response) {
        subscription = createSubscription({
          ...response,
          scope,
          principal,
        });
      }

      callback.onPushSubscription(Cr.NS_OK, subscription);
    } catch (e) {
      callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null);
    }
  }

  clearForDomain(domain, originAttributesPattern, callback) {
    callback.onClear(Cr.NS_OK);
  }

  clearForPrincipal(principal, callback) {
    callback.onClear(Cr.NS_OK);
  }

  // nsIPushQuotaManager methods

  notificationForOriginShown() {}

  notificationForOriginClosed() {}

  // nsIPushErrorReporter methods

  reportDeliveryError() {}
}

PushService.prototype.classID = Components.ID(
  "{a54d84d7-98a4-4fec-b664-e42e512ae9cc}"
);
PushService.prototype.contractID = "@mozilla.org/push/Service;1";
PushService.prototype.QueryInterface = ChromeUtils.generateQI([
  "nsIObserver",
  "nsISupportsWeakReference",
  "nsIPushService",
  "nsIPushQuotaManager",
  "nsIPushErrorReporter",
]);

/** `PushSubscription` instances are passed to all subscription callbacks. */
class PushSubscription {
  constructor(props) {
    this._props = props;
  }

  /** The URL for sending messages to this subscription. */
  get endpoint() {
    return this._props.endpoint;
  }

  /** The last time a message was sent to this subscription. */
  get lastPush() {
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  /** The total number of messages sent to this subscription. */
  get pushCount() {
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  /**
   * The app will take care of throttling, so we don't
   * care about the quota stuff here.
   */
  get quota() {
    return -1;
  }

  /**
   * Indicates whether this subscription was created with the system principal.
   * System subscriptions are exempt from the background message quota and
   * permission checks.
   */
  get isSystemSubscription() {
    return false;
  }

  /** The private key used to decrypt incoming push messages, in JWK format */
  get p256dhPrivateKey() {
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  /**
   * Indicates whether this subscription is subject to the background message
   * quota.
   */
  quotaApplies() {
    return false;
  }

  /**
   * Indicates whether this subscription exceeded the background message quota,
   * or the user revoked the notification permission. The caller must request a
   * new subscription to continue receiving push messages.
   */
  isExpired() {
    return false;
  }

  /**
   * Returns a key for encrypting messages sent to this subscription. JS
   * callers receive the key buffer as a return value, while C++ callers
   * receive the key size and buffer as out parameters.
   */
  getKey(name) {
    switch (name) {
      case "p256dh":
        return this._getRawKey(this._props.p256dhKey);

      case "auth":
        return this._getRawKey(this._props.authenticationSecret);

      case "appServer":
        return this._getRawKey(this._props.appServerKey);
    }
    return [];
  }

  _getRawKey(key) {
    if (!key) {
      return [];
    }
    return new Uint8Array(key);
  }
}

PushSubscription.prototype.QueryInterface = ChromeUtils.generateQI([
  "nsIPushSubscription",
]);
