/**
 * notion-enhancer: api
 * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
 * (https://notion-enhancer.github.io/) under the MIT license
 */

'use strict';

/**
 * a basic wrapper around notion's content apis
 * @namespace notion
 */

import { web, fs, fmt } from './index.mjs';

const standardiseUUID = (uuid) => {
  if (uuid?.length === 32 && !uuid.includes('-')) {
    uuid = uuid.replace(
      /([\d\w]{8})([\d\w]{4})([\d\w]{4})([\d\w]{4})([\d\w]{12})/,
      '$1-$2-$3-$4-$5'
    );
  }
  return uuid;
};

/**
 * unofficial content api: get a block by id
 * (requires user to be signed in or content to be public).
 * why not use the official api?
 * 1. cors blocking prevents use on the client
 * 2. the majority of blocks are still 'unsupported'
 * @param {string} id - uuidv4 record id
 * @param {string=} table - record type (default: 'block').
 * may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
 * @returns {Promise<object>} record data. type definitions can be found here:
 * https://github.com/NotionX/react-notion-x/tree/master/packages/notion-types/src
 */
export const get = async (id, table = 'block') => {
  id = standardiseUUID(id);
  const json = await fs.getJSON('https://www.notion.so/api/v3/getRecordValues', {
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ requests: [{ table, id }] }),
    method: 'POST',
  });
  return json?.results?.[0]?.value || json;
};

/**
 * get the id of the current user (requires user to be signed in)
 * @returns {string} uuidv4 user id
 */
export const getUserID = () =>
  JSON.parse(localStorage['LRU:KeyValueStore2:current-user-id'] || {}).value;

/**
 * get the id of the currently open page
 * @returns {string} uuidv4 page id
 */
export const getPageID = () =>
  standardiseUUID(
    web.queryParams().get('p') || location.pathname.split(/(-|\/)/g).reverse()[0]
  );

let _spaceID;
/**
 * get the id of the currently open workspace (requires user to be signed in)
 * @returns {string} uuidv4 space id
 */
export const getSpaceID = async () => {
  if (!_spaceID) _spaceID = (await get(getPageID())).space_id;
  return _spaceID;
};

/**
 * unofficial content api: search all blocks in a space
 * (requires user to be signed in or content to be public).
 * why not use the official api?
 * 1. cors blocking prevents use on the client
 * 2. the majority of blocks are still 'unsupported'
 * @param {string=} query - query to search blocks in the space for
 * @param {number=} limit - the max number of results to return (default: 20)
 * @param {string=} spaceID - uuidv4 workspace id
 * @returns {object} the number of total results, the list of matches, and related record values.
 * type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/api.ts
 */
export const search = async (query = '', limit = 20, spaceID = getSpaceID()) => {
  spaceID = standardiseUUID(await spaceID);
  const json = await fs.getJSON('https://www.notion.so/api/v3/search', {
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      type: 'BlocksInSpace',
      query,
      spaceId: spaceID,
      limit,
      filters: {
        isDeletedOnly: false,
        excludeTemplates: false,
        isNavigableOnly: false,
        requireEditPermissions: false,
        ancestors: [],
        createdBy: [],
        editedBy: [],
        lastEditedTime: {},
        createdTime: {},
      },
      sort: 'Relevance',
      source: 'quick_find',
    }),
    method: 'POST',
  });
  return json;
};

/**
 * unofficial content api: update a property/the content of an existing record
 * (requires user to be signed in or content to be public).
 * TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
 * to be unable to parse and render content properly and throw errors.
 * why not use the official api?
 * 1. cors blocking prevents use on the client
 * 2. the majority of blocks are still 'unsupported'
 * @param {object} pointer - the record being updated
 * @param {object} recordValue - the new raw data values to set to the record.
 * for examples, use notion.get to fetch an existing block record.
 * to use this to update content, set pointer.path to ['properties', 'title]
 * and recordValue to an array of rich text segments. a segment is an array
 * where the first value is the displayed text and the second value
 * is an array of decorations. a decoration is an array where the first value
 * is a modifier and the second value specifies it. e.g.
 * [
 *   ['bold text', [['b']]],
 *   [' '],
 *   ['an italicised link', [['i'], ['a', 'https://github.com']]],
 *   [' '],
 *   ['highlighted text', [['h', 'pink_background']]],
 * ]
 * more examples can be creating a block with the desired content/formatting,
 * then find the value of blockRecord.properties.title using notion.get.
 * type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/core.ts
 * @param {string} pointer.recordID - uuidv4 record id
 * @param {string=} pointer.recordTable - record type (default: 'block').
 * may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
 * @param {string=} pointer.property - the record property to update.
 * for record content, it will be the default: 'title'.
 * for page properties, it will be the property id (the key used in pageRecord.properties).
 * other possible values are unknown/untested
 * @param {string=} pointer.spaceID - uuidv4 workspace id
 * @param {string=} pointer.path - the path to the key to be set within the record
 * (default: [], the root of the record's values)
 * @returns {boolean|object} true if success, else an error object
 */
export const set = async (
  { recordID, recordTable = 'block', spaceID = getSpaceID(), path = [] },
  recordValue = {}
) => {
  spaceID = standardiseUUID(await spaceID);
  recordID = standardiseUUID(recordID);
  const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', {
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      requestId: fmt.uuidv4(),
      transactions: [
        {
          id: fmt.uuidv4(),
          spaceId: spaceID,
          operations: [
            {
              pointer: {
                table: recordTable,
                id: recordID,
                spaceId: spaceID,
              },
              path,
              command: path.length ? 'set' : 'update',
              args: recordValue,
            },
          ],
        },
      ],
    }),
    method: 'POST',
  });
  return json.errorId ? json : true;
};

/**
 * unofficial content api: create and add a new block to a page
 * (requires user to be signed in or content to be public).
 * TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
 * to be unable to parse and render content properly and throw errors.
 * why not use the official api?
 * 1. cors blocking prevents use on the client
 * 2. the majority of blocks are still 'unsupported'
 * @param {object} insert - the new record.
 * @param {object} pointer - where to insert the new block
 * for examples, use notion.get to fetch an existing block record.
 * type definitions can be found here: https://github.com/NotionX/react-notion-x/blob/master/packages/notion-types/src/block.ts
 * may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
 * @param {object=} insert.recordValue - the new raw data values to set to the record.
 * @param {object=} insert.recordTable - record type (default: 'block').
 * may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
 * @param {string=} pointer.prepend - insert before pointer.siblingID. if false, will be appended after
 * @param {string=} pointer.siblingID - uuidv4 sibling id. if unset, the record will be
 * inserted at the end of the page start (or the start if pointer.prepend is true)
 * @param {string=} pointer.parentID - uuidv4 parent id
 * @param {string=} pointer.parentTable - parent record type (default: 'block').
 * @param {string=} pointer.spaceID - uuidv4 space id
 * @param {string=} pointer.userID - uuidv4 user id
 * instead of the end
 * @returns {string|object} error object or uuidv4 of the new record
 */
export const create = async (
  { recordValue = {}, recordTable = 'block' } = {},
  {
    prepend = false,
    siblingID = undefined,
    parentID = getPageID(),
    parentTable = 'block',
    spaceID = getSpaceID(),
    userID = getUserID(),
  } = {}
) => {
  spaceID = standardiseUUID(await spaceID);
  parentID = standardiseUUID(parentID);
  siblingID = standardiseUUID(siblingID);
  const recordID = standardiseUUID(recordValue?.id ?? fmt.uuidv4()),
    path = [],
    args = {
      type: 'text',
      id: recordID,
      version: 0,
      created_time: new Date().getTime(),
      last_edited_time: new Date().getTime(),
      parent_id: parentID,
      parent_table: parentTable,
      alive: true,
      created_by_table: 'notion_user',
      created_by_id: userID,
      last_edited_by_table: 'notion_user',
      last_edited_by_id: userID,
      space_id: spaceID,
      permissions: [{ type: 'user_permission', role: 'editor', user_id: userID }],
    };
  if (parentTable === 'space') {
    parentID = spaceID;
    args.parent_id = spaceID;
    path.push('pages');
    args.type = 'page';
  } else if (parentTable === 'collection_view') {
    path.push('page_sort');
    args.type = 'page';
  } else {
    path.push('content');
  }
  const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', {
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      requestId: fmt.uuidv4(),
      transactions: [
        {
          id: fmt.uuidv4(),
          spaceId: spaceID,
          operations: [
            {
              pointer: {
                table: parentTable,
                id: parentID,
                spaceId: spaceID,
              },
              path,
              command: prepend ? 'listBefore' : 'listAfter',
              args: {
                ...(siblingID ? { after: siblingID } : {}),
                id: recordID,
              },
            },
            {
              pointer: {
                table: recordTable,
                id: recordID,
                spaceId: spaceID,
              },
              path: [],
              command: 'set',
              args: {
                ...args,
                ...recordValue,
              },
            },
          ],
        },
      ],
    }),
    method: 'POST',
  });
  return json.errorId ? json : recordID;
};

/**
 * unofficial content api: upload a file to notion's aws servers
 * (requires user to be signed in or content to be public).
 * TEST THIS THOROUGHLY. misuse can corrupt a record, leading the notion client
 * to be unable to parse and render content properly and throw errors.
 * why not use the official api?
 * 1. cors blocking prevents use on the client
 * 2. the majority of blocks are still 'unsupported'
 * @param {File} file - the file to upload
 * @param {object=} pointer - where the file should be accessible from
 * @param {string=} pointer.pageID - uuidv4 page id
 * @param {string=} pointer.spaceID - uuidv4 space id
 * @returns {string|object} error object or the url of the uploaded file
 */
export const upload = async (file, { pageID = getPageID(), spaceID = getSpaceID() } = {}) => {
  spaceID = standardiseUUID(await spaceID);
  pageID = standardiseUUID(pageID);
  const json = await fs.getJSON('https://www.notion.so/api/v3/getUploadFileUrl', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      bucket: 'secure',
      name: file.name,
      contentType: file.type,
      record: {
        table: 'block',
        id: pageID,
        spaceId: spaceID,
      },
    }),
  });
  if (json.errorId) return json;
  fetch(json.signedPutUrl, {
    method: 'PUT',
    headers: { 'content-type': file.type },
    body: file,
  });
  return json.url;
};

/**
 * redirect through notion to a resource's signed aws url for display outside of notion
 * (requires user to be signed in or content to be public)
 * @param src source url for file
 * @param {string} recordID uuidv4 record/block/file id
 * @param {string=} recordTable record type (default: 'block').
 * may also be 'collection', 'collection_view', 'space', 'notion_user', 'discussion', or 'comment'
 * @returns {string} url signed if necessary, else string as-is
 */
export const sign = (src, recordID, recordTable = 'block') => {
  if (src.startsWith('/')) src = `https://notion.so${src}`;
  if (src.includes('secure.notion-static.com')) {
    src = new URL(src);
    src = `https://www.notion.so/signed/${encodeURIComponent(
      src.origin + src.pathname
    )}?table=${recordTable}&id=${recordID}`;
  }
  return src;
};
