'use strict';

/**
 * 2014 John McLear (Etherpad Foundation / McLear Ltd)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const AttributePool = require('../../static/js/AttributePool');
const {Pad} = require('../db/Pad');
const Stream = require('./Stream');
const authorManager = require('../db/AuthorManager');
const db = require('../db/DB');
const hooks = require('../../static/js/pluginfw/hooks');
const log4js = require('log4js');
const supportedElems = require('../../static/js/contentcollector').supportedElems;
const ueberdb = require('ueberdb2');

const logger = log4js.getLogger('ImportEtherpad');

exports.setPadRaw = async (padId, r, authorId = '') => {
  const records = JSON.parse(r);

  // get supported block Elements from plugins, we will use this later.
  hooks.callAll('ccRegisterBlockElements').forEach((element) => {
    supportedElems.add(element);
  });

  // DB key prefixes for pad records. Each key is expected to have the form `${prefix}:${padId}` or
  // `${prefix}:${padId}:${otherstuff}`.
  const padKeyPrefixes = [
    ...await hooks.aCallAll('exportEtherpadAdditionalContent'),
    'pad',
  ];

  let originalPadId = null;
  const checkOriginalPadId = (padId) => {
    if (originalPadId == null) originalPadId = padId;
    if (originalPadId !== padId) throw new Error('unexpected pad ID in record');
  };

  // First validate and transform values. Do not commit any records to the database yet in case
  // there is a problem with the data.

  const data = new Map();
  const existingAuthors = new Set();
  const padDb = new ueberdb.Database('memory', {data});
  await padDb.init();
  try {
    const processRecord = async (key, value) => {
      if (!value) return;
      const keyParts = key.split(':');
      const [prefix, id] = keyParts;
      if (prefix === 'globalAuthor' && keyParts.length === 2) {
        // In the database, the padIDs subkey is an object (which is used as a set) that records
        // every pad the author has worked on. When exported, that object becomes a single string
        // containing the exported pad's ID.
        if (typeof value.padIDs !== 'string') {
          throw new TypeError('globalAuthor padIDs subkey is not a string');
        }
        checkOriginalPadId(value.padIDs);
        if (await authorManager.doesAuthorExist(id)) {
          existingAuthors.add(id);
          return;
        }
        value.padIDs = {[padId]: 1};
      } else if (padKeyPrefixes.includes(prefix)) {
        checkOriginalPadId(id);
        if (prefix === 'pad' && keyParts.length === 2) {
          const pool = new AttributePool().fromJsonable(value.pool);
          const unsupportedElements = new Set();
          pool.eachAttrib((k, v) => {
            if (!supportedElems.has(k)) unsupportedElements.add(k);
          });
          if (unsupportedElements.size) {
            logger.warn(`(pad ${padId}) unsupported attributes (try installing a plugin): ` +
                        `${[...unsupportedElements].join(', ')}`);
          }
        }
        keyParts[1] = padId;
        key = keyParts.join(':');
      } else {
        logger.debug(`(pad ${padId}) The record with the following key will be ignored unless an ` +
                     `importEtherpad hook function processes it: ${key}`);
        return;
      }
      await padDb.set(key, value);
    };
    const readOps = new Stream(Object.entries(records)).map(([k, v]) => processRecord(k, v));
    for (const op of readOps.batch(100).buffer(99)) await op;

    const pad = new Pad(padId, padDb);
    await pad.init(null, authorId);
    await hooks.aCallAll('importEtherpad', {
      pad,
      // Shallow freeze meant to prevent accidental bugs. It would be better to deep freeze, but
      // it's not worth the added complexity.
      data: Object.freeze(records),
      srcPadId: originalPadId,
    });
    await pad.check();
  } finally {
    await padDb.close();
  }

  const writeOps = (function* () {
    for (const [k, v] of data) yield db.set(k, v);
    for (const a of existingAuthors) yield authorManager.addPad(a, padId);
  })();
  for (const op of new Stream(writeOps).batch(100).buffer(99)) await op;
};
