import Router from "koa-router";
import { isUndefined } from "lodash";
import { Op, WhereOptions } from "sequelize";
import { NotFoundError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { Document, User, Event, Share, Team, Collection } from "@server/models";
import { authorize } from "@server/policies";
import { presentShare, presentPolicies } from "@server/presenters";
import { APIContext } from "@server/types";
import { assertUuid, assertSort, assertPresent } from "@server/validation";
import pagination from "./middlewares/pagination";

const router = new Router();

router.post("shares.info", auth(), async (ctx: APIContext) => {
  const { id, documentId } = ctx.request.body;
  assertPresent(id || documentId, "id or documentId is required");
  if (id) {
    assertUuid(id, "id is must be a uuid");
  }
  if (documentId) {
    assertUuid(documentId, "documentId is must be a uuid");
  }

  const { user } = ctx.state.auth;
  const shares = [];
  const share = await Share.scope({
    method: ["withCollectionPermissions", user.id],
  }).findOne({
    where: id
      ? {
          id,
          revokedAt: {
            [Op.is]: null,
          },
        }
      : {
          documentId,
          teamId: user.teamId,
          revokedAt: {
            [Op.is]: null,
          },
        },
  });

  // We return the response for the current documentId and any parent documents
  // that are publicly shared and accessible to the user
  if (share && share.document) {
    authorize(user, "read", share);
    shares.push(share);
  }

  if (documentId) {
    const document = await Document.findByPk(documentId, {
      userId: user.id,
    });
    authorize(user, "read", document);

    const collection = await document.$get("collection");
    const parentIds = collection?.getDocumentParents(documentId);
    const parentShare = parentIds
      ? await Share.scope({
          method: ["withCollectionPermissions", user.id],
        }).findOne({
          where: {
            documentId: parentIds,
            teamId: user.teamId,
            revokedAt: {
              [Op.is]: null,
            },
            includeChildDocuments: true,
            published: true,
          },
        })
      : undefined;

    if (parentShare && parentShare.document) {
      authorize(user, "read", parentShare);
      shares.push(parentShare);
    }
  }

  if (!shares.length) {
    ctx.response.status = 204;
    return;
  }

  ctx.body = {
    data: {
      shares: shares.map((share) => presentShare(share, user.isAdmin)),
    },
    policies: presentPolicies(user, shares),
  };
});

router.post("shares.list", auth(), pagination(), async (ctx: APIContext) => {
  let { direction } = ctx.request.body;
  const { sort = "updatedAt" } = ctx.request.body;
  if (direction !== "ASC") {
    direction = "DESC";
  }
  assertSort(sort, Share);

  const { user } = ctx.state.auth;
  const where: WhereOptions<Share> = {
    teamId: user.teamId,
    userId: user.id,
    published: true,
    revokedAt: {
      [Op.is]: null,
    },
  };

  if (user.isAdmin) {
    delete where.userId;
  }

  const collectionIds = await user.collectionIds();

  const [shares, total] = await Promise.all([
    Share.findAll({
      where,
      order: [[sort, direction]],
      include: [
        {
          model: Document,
          required: true,
          paranoid: true,
          as: "document",
          where: {
            collectionId: collectionIds,
          },
          include: [
            {
              model: Collection.scope({
                method: ["withMembership", user.id],
              }),
              as: "collection",
            },
          ],
        },
        {
          model: User,
          required: true,
          as: "user",
        },
        {
          model: Team,
          required: true,
          as: "team",
        },
      ],
      offset: ctx.state.pagination.offset,
      limit: ctx.state.pagination.limit,
    }),
    Share.count({ where }),
  ]);

  ctx.body = {
    pagination: { ...ctx.state.pagination, total },
    data: shares.map((share) => presentShare(share, user.isAdmin)),
    policies: presentPolicies(user, shares),
  };
});

router.post("shares.update", auth(), async (ctx: APIContext) => {
  const { id, includeChildDocuments, published, urlId } = ctx.request.body;
  assertUuid(id, "id is required");

  const { user } = ctx.state.auth;
  const team = await Team.findByPk(user.teamId);
  authorize(user, "share", team);

  // fetch the share with document and collection.
  const share = await Share.scope({
    method: ["withCollectionPermissions", user.id],
  }).findByPk(id);

  authorize(user, "update", share);

  if (published !== undefined) {
    share.published = published;

    // Reset nested document sharing when unpublishing a share link. So that
    // If it's ever re-published this doesn't immediately share nested docs
    // without forewarning the user
    if (!published) {
      share.includeChildDocuments = false;
    }
  }

  if (includeChildDocuments !== undefined) {
    share.includeChildDocuments = includeChildDocuments;
  }

  if (!isUndefined(urlId)) {
    share.urlId = urlId;
  }

  await share.save();
  await Event.create({
    name: "shares.update",
    documentId: share.documentId,
    modelId: share.id,
    teamId: user.teamId,
    actorId: user.id,
    data: {
      published,
    },
    ip: ctx.request.ip,
  });

  ctx.body = {
    data: presentShare(share, user.isAdmin),
    policies: presentPolicies(user, [share]),
  };
});

router.post("shares.create", auth(), async (ctx: APIContext) => {
  const { documentId } = ctx.request.body;
  assertPresent(documentId, "documentId is required");

  const { user } = ctx.state.auth;
  const document = await Document.findByPk(documentId, {
    userId: user.id,
  });

  // user could be creating the share link to share with team members
  authorize(user, "read", document);

  const team = await Team.findByPk(user.teamId);

  const [share, isCreated] = await Share.findOrCreate({
    where: {
      documentId,
      teamId: user.teamId,
      revokedAt: null,
    },
    defaults: {
      userId: user.id,
    },
  });

  if (isCreated) {
    await Event.create({
      name: "shares.create",
      documentId,
      collectionId: document.collectionId,
      modelId: share.id,
      teamId: user.teamId,
      actorId: user.id,
      data: {
        name: document.title,
      },
      ip: ctx.request.ip,
    });
  }

  if (team) {
    share.team = team;
  }
  share.user = user;
  share.document = document;

  ctx.body = {
    data: presentShare(share),
    policies: presentPolicies(user, [share]),
  };
});

router.post("shares.revoke", auth(), async (ctx: APIContext) => {
  const { id } = ctx.request.body;
  assertUuid(id, "id is required");

  const { user } = ctx.state.auth;
  const share = await Share.findByPk(id);

  if (!share?.document) {
    throw NotFoundError();
  }

  authorize(user, "revoke", share);
  const { document } = share;

  await share.revoke(user.id);
  await Event.create({
    name: "shares.revoke",
    documentId: document.id,
    collectionId: document.collectionId,
    modelId: share.id,
    teamId: user.teamId,
    actorId: user.id,
    data: {
      name: document.title,
    },
    ip: ctx.request.ip,
  });

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

export default router;
