// @flow
import Router from 'koa-router';
import Sequelize from 'sequelize';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import { presentDocument, presentRevision } from '../presenters';
import { Document, Collection, Share, Star, View, Revision } from '../models';
import { InvalidRequestError } from '../errors';
import events from '../events';
import policy from '../policies';

const Op = Sequelize.Op;
const { authorize, cannot } = policy;
const router = new Router();

router.post('documents.list', auth(), pagination(), async ctx => {
  let { sort = 'updatedAt', direction, collection } = ctx.body;
  if (direction !== 'ASC') direction = 'DESC';

  const user = ctx.state.user;
  let where = { teamId: user.teamId };
  if (collection) where = { ...where, atlasId: collection };

  const starredScope = { method: ['withStarred', user.id] };
  const documents = await Document.scope('defaultScope', starredScope).findAll({
    where,
    order: [[sort, direction]],
    offset: ctx.state.pagination.offset,
    limit: ctx.state.pagination.limit,
  });

  const data = await Promise.all(
    documents.map(document => presentDocument(ctx, document))
  );

  ctx.body = {
    pagination: ctx.state.pagination,
    data,
  };
});

router.post('documents.pinned', auth(), pagination(), async ctx => {
  let { sort = 'updatedAt', direction, collection } = ctx.body;
  if (direction !== 'ASC') direction = 'DESC';
  ctx.assertPresent(collection, 'collection is required');

  const user = ctx.state.user;
  const starredScope = { method: ['withStarred', user.id] };
  const documents = await Document.scope('defaultScope', starredScope).findAll({
    where: {
      teamId: user.teamId,
      atlasId: collection,
      pinnedById: {
        // $FlowFixMe
        [Op.ne]: null,
      },
    },
    order: [[sort, direction]],
    offset: ctx.state.pagination.offset,
    limit: ctx.state.pagination.limit,
  });

  const data = await Promise.all(
    documents.map(document => presentDocument(ctx, document))
  );

  ctx.body = {
    pagination: ctx.state.pagination,
    data,
  };
});

router.post('documents.viewed', auth(), pagination(), async ctx => {
  let { sort = 'updatedAt', direction } = ctx.body;
  if (direction !== 'ASC') direction = 'DESC';

  const user = ctx.state.user;
  const views = await View.findAll({
    where: { userId: user.id },
    order: [[sort, direction]],
    include: [
      {
        model: Document,
        required: true,
        include: [
          {
            model: Star,
            as: 'starred',
            where: { userId: user.id },
            required: false,
          },
        ],
      },
    ],
    offset: ctx.state.pagination.offset,
    limit: ctx.state.pagination.limit,
  });

  const data = await Promise.all(
    views.map(view => presentDocument(ctx, view.document))
  );

  ctx.body = {
    pagination: ctx.state.pagination,
    data,
  };
});

router.post('documents.starred', auth(), pagination(), async ctx => {
  let { sort = 'updatedAt', direction } = ctx.body;
  if (direction !== 'ASC') direction = 'DESC';

  const user = ctx.state.user;
  const views = await Star.findAll({
    where: { userId: user.id },
    order: [[sort, direction]],
    include: [
      {
        model: Document,
        include: [{ model: Star, as: 'starred', where: { userId: user.id } }],
      },
    ],
    offset: ctx.state.pagination.offset,
    limit: ctx.state.pagination.limit,
  });

  const data = await Promise.all(
    views.map(view => presentDocument(ctx, view.document))
  );

  ctx.body = {
    pagination: ctx.state.pagination,
    data,
  };
});

router.post('documents.drafts', auth(), pagination(), async ctx => {
  let { sort = 'updatedAt', direction } = ctx.body;
  if (direction !== 'ASC') direction = 'DESC';

  const user = ctx.state.user;
  const documents = await Document.findAll({
    // $FlowFixMe
    where: { userId: user.id, publishedAt: { [Op.eq]: null } },
    order: [[sort, direction]],
    offset: ctx.state.pagination.offset,
    limit: ctx.state.pagination.limit,
  });

  const data = await Promise.all(
    documents.map(document => presentDocument(ctx, document))
  );

  ctx.body = {
    pagination: ctx.state.pagination,
    data,
  };
});

router.post('documents.info', auth({ required: false }), async ctx => {
  const { id, shareId } = ctx.body;
  ctx.assertPresent(id || shareId, 'id or shareId is required');

  const user = ctx.state.user;
  let document;

  if (shareId) {
    const share = await Share.find({
      where: {
        // $FlowFixMe
        revokedAt: { [Op.eq]: null },
        id: shareId,
      },
      include: [
        {
          model: Document,
          required: true,
          as: 'document',
        },
      ],
    });
    if (!share) {
      throw new InvalidRequestError('Document could not be found for shareId');
    }
    document = share.document;
  } else {
    document = await Document.findById(id);
    authorize(user, 'read', document);
  }

  const isPublic = cannot(user, 'read', document);

  ctx.body = {
    data: await presentDocument(ctx, document, { isPublic }),
  };
});

router.post('documents.revisions', auth(), pagination(), async ctx => {
  let { id, sort = 'updatedAt', direction } = ctx.body;
  if (direction !== 'ASC') direction = 'DESC';
  ctx.assertPresent(id, 'id is required');
  const document = await Document.findById(id);

  authorize(ctx.state.user, 'read', document);

  const revisions = await Revision.findAll({
    where: { documentId: id },
    order: [[sort, direction]],
    offset: ctx.state.pagination.offset,
    limit: ctx.state.pagination.limit,
  });

  const data = await Promise.all(
    revisions.map(revision => presentRevision(ctx, revision))
  );

  ctx.body = {
    pagination: ctx.state.pagination,
    data,
  };
});

router.post('documents.search', auth(), pagination(), async ctx => {
  const { query } = ctx.body;
  const { offset, limit } = ctx.state.pagination;
  ctx.assertPresent(query, 'query is required');

  const user = ctx.state.user;
  const documents = await Document.searchForUser(user, query, {
    offset,
    limit,
  });

  const data = await Promise.all(
    documents.map(async document => await presentDocument(ctx, document))
  );

  ctx.body = {
    pagination: ctx.state.pagination,
    data,
  };
});

router.post('documents.pin', auth(), async ctx => {
  const { id } = ctx.body;
  ctx.assertPresent(id, 'id is required');
  const user = ctx.state.user;
  const document = await Document.findById(id);

  authorize(user, 'update', document);

  document.pinnedById = user.id;
  await document.save();

  ctx.body = {
    data: await presentDocument(ctx, document),
  };
});

router.post('documents.unpin', auth(), async ctx => {
  const { id } = ctx.body;
  ctx.assertPresent(id, 'id is required');
  const user = ctx.state.user;
  const document = await Document.findById(id);

  authorize(user, 'update', document);

  document.pinnedById = null;
  await document.save();

  ctx.body = {
    data: await presentDocument(ctx, document),
  };
});

router.post('documents.star', auth(), async ctx => {
  const { id } = ctx.body;
  ctx.assertPresent(id, 'id is required');
  const user = ctx.state.user;
  const document = await Document.findById(id);

  authorize(user, 'read', document);

  await Star.findOrCreate({
    where: { documentId: document.id, userId: user.id },
  });
});

router.post('documents.unstar', auth(), async ctx => {
  const { id } = ctx.body;
  ctx.assertPresent(id, 'id is required');
  const user = ctx.state.user;
  const document = await Document.findById(id);

  authorize(user, 'read', document);

  await Star.destroy({
    where: { documentId: document.id, userId: user.id },
  });
});

router.post('documents.create', auth(), async ctx => {
  const { title, text, publish, parentDocument, index } = ctx.body;
  const collectionId = ctx.body.collection;
  ctx.assertPresent(collectionId, 'collection is required');
  ctx.assertUuid(collectionId, 'collection must be an uuid');
  ctx.assertPresent(title, 'title is required');
  ctx.assertPresent(text, 'text is required');
  if (parentDocument)
    ctx.assertUuid(parentDocument, 'parentDocument must be an uuid');
  if (index) ctx.assertPositiveInteger(index, 'index must be an integer (>=0)');

  const user = ctx.state.user;
  authorize(user, 'create', Document);

  const collection = await Collection.findOne({
    where: {
      id: collectionId,
      teamId: user.teamId,
    },
  });
  authorize(user, 'publish', collection);

  let parentDocumentObj = {};
  if (parentDocument && collection.type === 'atlas') {
    parentDocumentObj = await Document.findOne({
      where: {
        id: parentDocument,
        atlasId: collection.id,
      },
    });
    authorize(user, 'read', parentDocumentObj);
  }

  let document = await Document.create({
    parentDocumentId: parentDocumentObj.id,
    atlasId: collection.id,
    teamId: user.teamId,
    userId: user.id,
    lastModifiedById: user.id,
    createdById: user.id,
    title,
    text,
  });

  if (publish) {
    await document.publish();
  }

  // reload to get all of the data needed to present (user, collection etc)
  // we need to specify publishedAt to bypass default scope that only returns
  // published documents
  document = await Document.find({
    where: { id: document.id, publishedAt: document.publishedAt },
  });

  ctx.body = {
    data: await presentDocument(ctx, document),
  };
});

router.post('documents.update', auth(), async ctx => {
  const { id, title, text, publish, autosave, done, lastRevision } = ctx.body;
  ctx.assertPresent(id, 'id is required');
  ctx.assertPresent(title || text, 'title or text is required');

  const user = ctx.state.user;
  const document = await Document.findById(id);

  authorize(ctx.state.user, 'update', document);

  if (lastRevision && lastRevision !== document.revisionCount) {
    throw new InvalidRequestError('Document has changed since last revision');
  }

  // Update document
  if (title) document.title = title;
  if (text) document.text = text;
  document.lastModifiedById = user.id;

  if (publish) {
    await document.publish();
  } else {
    await document.save({ autosave });

    if (document.publishedAt && done) {
      events.add({ name: 'documents.update', model: document });
    }
  }

  ctx.body = {
    data: await presentDocument(ctx, document),
  };
});

router.post('documents.move', auth(), async ctx => {
  const { id, parentDocument, index } = ctx.body;
  ctx.assertPresent(id, 'id is required');
  if (parentDocument)
    ctx.assertUuid(parentDocument, 'parentDocument must be a uuid');
  if (index) ctx.assertPositiveInteger(index, 'index must be an integer (>=0)');

  const user = ctx.state.user;
  const document = await Document.findById(id);
  authorize(user, 'update', document);

  const collection = document.collection;
  if (collection.type !== 'atlas')
    throw new InvalidRequestError('This document can’t be moved');

  // Set parent document
  if (parentDocument) {
    const parent = await Document.findById(parentDocument);
    authorize(user, 'update', parent);
  }

  if (parentDocument === id)
    throw new InvalidRequestError('Infinite loop detected and prevented!');

  // If no parent document is provided, set it as null (move to root level)
  document.parentDocumentId = parentDocument;
  await document.save();

  await collection.moveDocument(document, index);
  // Update collection
  document.collection = collection;

  ctx.body = {
    data: await presentDocument(ctx, document),
  };
});

router.post('documents.delete', auth(), async ctx => {
  const { id } = ctx.body;
  ctx.assertPresent(id, 'id is required');

  const document = await Document.findById(id);
  authorize(ctx.state.user, 'delete', document);

  const collection = document.collection;
  if (collection && collection.type === 'atlas') {
    // Delete document and all of its children
    await collection.removeDocument(document);
  }

  await document.destroy();

  ctx.body = {
    success: true,
  };
});

export default router;
