/* 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, {
  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
});

const COLLECTION_NAME = "tracking-protection-lists";

// SafeBrowsing protocol parameters.
export const SBRS_UPDATE_MINIMUM_DELAY = 21600; // Minimum delay before polling again in seconds

export function UrlClassifierRemoteSettingsService() {}
UrlClassifierRemoteSettingsService.prototype = {
  classID: Components.ID("{1980624c-c50b-4b46-a91c-dfaba7792706}"),
  QueryInterface: ChromeUtils.generateQI([
    "nsIUrlClassifierRemoteSettingsService",
  ]),

  _initialized: false,

  // Entries that are retrieved from RemoteSettings.get(). keyed by the table name.
  _entries: {},

  async lazyInit() {
    if (this._initialized) {
      return;
    }

    let rs = lazy.RemoteSettings(COLLECTION_NAME);
    // Bug 1750191: Notify listmanager to trigger an update instead
    // of polling data periodically.
    rs.on("sync", async event => {
      let {
        data: { current },
      } = event;
      this._onUpdateEntries(current);
    });

    this._initialized = true;

    let entries;
    try {
      entries = await rs.get();
    } catch (e) {}

    this._onUpdateEntries(entries || []);
  },

  _onUpdateEntries(aEntries) {
    aEntries.map(entry => {
      this._entries[entry.Name] = entry;
    });
  },

  // Parse the update request. See UrlClassifierListManager.sys.mjs makeUpdateRequest
  // for more details about how we build the update request.
  //
  // @param aRequest the request payload of the update request
  // @return array The array of requested tables. Each item in the array is composed
  //               with [table name, chunk numner]
  _parseRequest(aRequest) {
    let lines = aRequest.split("\n");
    let requests = [];
    for (let line of lines) {
      let fields = line.split(";");
      let chunkNum = fields[1]?.match(/(?<=a:).*/);
      requests.push([fields[0], chunkNum]);
    }
    return requests;
  },

  async _getLists(aRequest, aListener) {
    await this.lazyInit();

    let rs = lazy.RemoteSettings(COLLECTION_NAME);
    let payload = "n:" + SBRS_UPDATE_MINIMUM_DELAY + "\n";

    let requests = this._parseRequest(aRequest);
    for (let request of requests) {
      let [reqTableName, reqChunkNum] = request;
      let entry = this._entries[reqTableName];
      if (!entry?.attachment) {
        continue;
      }

      // If the request version is the same as what we have in Remote Settings,
      // we are up-to-date now.
      if (entry.Version == reqChunkNum) {
        continue;
      }

      let downloadError = false;
      try {
        // SafeBrowsing maintains its own files, so we can remove the downloaded
        // files after SafeBrowsing processes the data.
        let buffer = await rs.attachments.downloadAsBytes(entry);
        let bytes = new Uint8Array(buffer);
        let strData = "";
        for (let i = 0; i < bytes.length; i++) {
          strData += String.fromCharCode(bytes[i]);
        }

        // Construct the payload
        payload += "i:" + reqTableName + "\n";
        // Add adddel directive to clear the old chunks.
        if (reqChunkNum) {
          payload += "ad:" + reqChunkNum.toString() + "\n";
        }
        payload += strData;
      } catch (e) {
        downloadError = true;
      }

      if (downloadError) {
        try {
          aListener.onStartRequest(null);
          aListener.onStopRequest(null, Cr.NS_ERROR_FAILURE);
        } catch (e) {}
        return;
      }
    }

    // Send the update response over stream listener interface
    let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
      Ci.nsIStringInputStream
    );
    stream.setByteStringData(payload);

    try {
      aListener.onStartRequest(null);
      aListener.onDataAvailable(null, stream, 0, payload.length);
      aListener.onStopRequest(null, Cr.NS_OK);
    } catch (e) {}
  },

  fetchList(aPayload, aListener) {
    this._getLists(aPayload, aListener);
  },

  clear() {
    this._initialized = false;
    this._entries = {};
  },
};
