import moment from 'moment';
import Users from '../../models/users';
import Boards from '../../models/boards';
import Lists from '../../models/lists';
import Swimlanes from '../../models/swimlanes';
import Cards from '../../models/cards';
import CardComments from '../../models/cardComments';
import Attachments from '../../models/attachments';
import Checklists from '../../models/checklists';
import ChecklistItems from '../../models/checklistItems';
import SessionData from '../../models/usersessiondata';
import CustomFields from '../../models/customFields';
import {
  DEFAULT_LIMIT,
  OPERATOR_ASSIGNEE,
  OPERATOR_BOARD,
  OPERATOR_COMMENT,
  OPERATOR_DUE,
  OPERATOR_HAS,
  OPERATOR_LABEL,
  OPERATOR_LIMIT,
  OPERATOR_LIST,
  OPERATOR_MEMBER,
  OPERATOR_SORT,
  OPERATOR_STATUS,
  OPERATOR_SWIMLANE,
  OPERATOR_USER,
  ORDER_ASCENDING,
  PREDICATE_ALL,
  PREDICATE_ARCHIVED,
  PREDICATE_ASSIGNEES,
  PREDICATE_ATTACHMENT,
  PREDICATE_CHECKLIST,
  PREDICATE_CREATED_AT,
  PREDICATE_DESCRIPTION,
  PREDICATE_DUE_AT,
  PREDICATE_END_AT,
  PREDICATE_ENDED,
  PREDICATE_MEMBERS,
  PREDICATE_MODIFIED_AT,
  PREDICATE_PRIVATE,
  PREDICATE_PUBLIC,
  PREDICATE_START_AT,
  PREDICATE_SYSTEM,
} from '../../config/search-const';
import { QueryErrors, QueryParams, Query } from '../../config/query-classes';

const escapeForRegex = require('escape-string-regexp');

Meteor.publish('card', cardId => {
  check(cardId, String);
  return Cards.find({ _id: cardId });
});

Meteor.publish('myCards', function(sessionId) {
  const queryParams = new QueryParams();
  queryParams.addPredicate(OPERATOR_USER, Meteor.user().username);

  return findCards(sessionId, buildQuery(queryParams));
});

// Meteor.publish('dueCards', function(sessionId, allUsers = false) {
//   check(sessionId, String);
//   check(allUsers, Boolean);
//
//   // eslint-disable-next-line no-console
//   // console.log('all users:', allUsers);
//
//   const queryParams = {
//     has: [{ field: 'dueAt', exists: true }],
//     limit: 25,
//     skip: 0,
//     sort: { name: 'dueAt', order: 'des' },
//   };
//
//   if (!allUsers) {
//     queryParams.users = [Meteor.user().username];
//   }
//
//   return buildQuery(sessionId, queryParams);
// });

Meteor.publish('globalSearch', function(sessionId, params, text) {
  check(sessionId, String);
  check(params, Object);
  check(text, String);

  // eslint-disable-next-line no-console
  // console.log('queryParams:', params);

  return findCards(sessionId, buildQuery(new QueryParams(params, text)));
});

function buildSelector(queryParams) {
  const userId = Meteor.userId();

  const errors = new QueryErrors();

  let selector = {};

  // eslint-disable-next-line no-console
  // console.log('queryParams:', queryParams);

  if (queryParams.selector) {
    selector = queryParams.selector;
  } else {
    const boardsSelector = {};

    let archived = false;
    let endAt = null;
    if (queryParams.hasOperator(OPERATOR_STATUS)) {
      queryParams.getPredicates(OPERATOR_STATUS).forEach(status => {
        if (status === PREDICATE_ARCHIVED) {
          archived = true;
        } else if (status === PREDICATE_ALL) {
          archived = null;
        } else if (status === PREDICATE_ENDED) {
          endAt = { $nin: [null, ''] };
        } else if ([PREDICATE_PRIVATE, PREDICATE_PUBLIC].includes(status)) {
          boardsSelector.permission = status;
        }
      });
    }
    selector = {
      type: 'cardType-card',
      // boardId: { $in: Boards.userBoardIds(userId) },
      $and: [],
    };

    if (archived !== null) {
      if (archived) {
        selector.boardId = {
          $in: Boards.userBoardIds(userId, null, boardsSelector),
        };
        selector.$and.push({
          $or: [
            {
              boardId: {
                $in: Boards.userBoardIds(userId, archived, boardsSelector),
              },
            },
            { swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } },
            { listId: { $in: Lists.archivedListIds() } },
            { archived: true },
          ],
        });
      } else {
        selector.boardId = {
          $in: Boards.userBoardIds(userId, false, boardsSelector),
        };
        selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
        selector.listId = { $nin: Lists.archivedListIds() };
        selector.archived = false;
      }
    } else {
      selector.boardId = {
        $in: Boards.userBoardIds(userId, null, boardsSelector),
      };
    }
    if (endAt !== null) {
      selector.endAt = endAt;
    }

    if (queryParams.hasOperator(OPERATOR_BOARD)) {
      const queryBoards = [];
      queryParams.hasOperator(OPERATOR_BOARD).forEach(query => {
        const boards = Boards.userSearch(userId, {
          title: new RegExp(escapeForRegex(query), 'i'),
        });
        if (boards.count()) {
          boards.forEach(board => {
            queryBoards.push(board._id);
          });
        } else {
          errors.addNotFound(OPERATOR_BOARD, query);
        }
      });

      selector.boardId.$in = queryBoards;
    }

    if (queryParams.hasOperator(OPERATOR_SWIMLANE)) {
      const querySwimlanes = [];
      queryParams.getPredicates(OPERATOR_SWIMLANE).forEach(query => {
        const swimlanes = Swimlanes.find({
          title: new RegExp(escapeForRegex(query), 'i'),
        });
        if (swimlanes.count()) {
          swimlanes.forEach(swim => {
            querySwimlanes.push(swim._id);
          });
        } else {
          errors.addNotFound(OPERATOR_SWIMLANE, query);
        }
      });

      // eslint-disable-next-line no-prototype-builtins
      if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) {
        selector.swimlaneId = { $in: [] };
      }
      selector.swimlaneId.$in = querySwimlanes;
    }

    if (queryParams.hasOperator(OPERATOR_LIST)) {
      const queryLists = [];
      queryParams.getPredicates(OPERATOR_LIST).forEach(query => {
        const lists = Lists.find({
          title: new RegExp(escapeForRegex(query), 'i'),
        });
        if (lists.count()) {
          lists.forEach(list => {
            queryLists.push(list._id);
          });
        } else {
          errors.addNotFound(OPERATOR_LIST, query);
        }
      });

      // eslint-disable-next-line no-prototype-builtins
      if (!selector.hasOwnProperty('listId')) {
        selector.listId = { $in: [] };
      }
      selector.listId.$in = queryLists;
    }

    if (queryParams.hasOperator(OPERATOR_COMMENT)) {
      const cardIds = CardComments.textSearch(
        userId,
        queryParams.getPredicates(OPERATOR_COMMENT),
        com => {
          return com.cardId;
        },
      );
      if (cardIds.length) {
        selector._id = { $in: cardIds };
      } else {
        queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => {
          errors.addNotFound(OPERATOR_COMMENT, comment);
        });
      }
    }

    [OPERATOR_DUE, 'createdAt', 'modifiedAt'].forEach(field => {
      if (queryParams.hasOperator(field)) {
        selector[field] = {};
        const predicate = queryParams.getPredicate(field);
        selector[field][predicate.operator] = new Date(predicate.value);
      }
    });

    const queryUsers = {};
    queryUsers[OPERATOR_ASSIGNEE] = [];
    queryUsers[OPERATOR_MEMBER] = [];

    if (queryParams.hasOperator(OPERATOR_USER)) {
      queryParams.getPredicates(OPERATOR_USER).forEach(username => {
        const user = Users.findOne({ username });
        if (user) {
          queryUsers[OPERATOR_MEMBER].push(user._id);
          queryUsers[OPERATOR_ASSIGNEE].push(user._id);
        } else {
          errors.addNotFound(OPERATOR_USER, username);
        }
      });
    }

    [OPERATOR_MEMBER, OPERATOR_ASSIGNEE].forEach(key => {
      if (queryParams.hasOperator(key)) {
        queryParams.getPredicates(key).forEach(query => {
          const users = Users.find({
            username: query,
          });
          if (users.count()) {
            users.forEach(user => {
              queryUsers[key].push(user._id);
            });
          } else {
            errors.addNotFound(key, query);
          }
        });
      }
    });

    if (
      queryUsers[OPERATOR_MEMBER].length &&
      queryUsers[OPERATOR_ASSIGNEE].length
    ) {
      selector.$and.push({
        $or: [
          { members: { $in: queryUsers[OPERATOR_MEMBER] } },
          { assignees: { $in: queryUsers[OPERATOR_ASSIGNEE] } },
        ],
      });
    } else if (queryUsers[OPERATOR_MEMBER].length) {
      selector.members = { $in: queryUsers[OPERATOR_MEMBER] };
    } else if (queryUsers[OPERATOR_ASSIGNEE].length) {
      selector.assignees = { $in: queryUsers[OPERATOR_ASSIGNEE] };
    }

    if (queryParams.hasOperator(OPERATOR_LABEL)) {
      queryParams.getPredicates(OPERATOR_LABEL).forEach(label => {
        const queryLabels = [];

        let boards = Boards.userSearch(userId, {
          labels: { $elemMatch: { color: label.toLowerCase() } },
        });

        if (boards.count()) {
          boards.forEach(board => {
            // eslint-disable-next-line no-console
            // console.log('board:', board);
            // eslint-disable-next-line no-console
            // console.log('board.labels:', board.labels);
            board.labels
              .filter(boardLabel => {
                return boardLabel.color === label.toLowerCase();
              })
              .forEach(boardLabel => {
                queryLabels.push(boardLabel._id);
              });
          });
        } else {
          // eslint-disable-next-line no-console
          // console.log('label:', label);
          const reLabel = new RegExp(escapeForRegex(label), 'i');
          // eslint-disable-next-line no-console
          // console.log('reLabel:', reLabel);
          boards = Boards.userSearch(userId, {
            labels: { $elemMatch: { name: reLabel } },
          });

          if (boards.count()) {
            boards.forEach(board => {
              board.labels
                .filter(boardLabel => {
                  if (!boardLabel.name) {
                    return false;
                  }
                  return boardLabel.name.match(reLabel);
                })
                .forEach(boardLabel => {
                  queryLabels.push(boardLabel._id);
                });
            });
          } else {
            errors.addNotFound(OPERATOR_LABEL, label);
          }
        }

        selector.labelIds = { $in: _.uniq(queryLabels) };
      });
    }

    if (queryParams.hasOperator(OPERATOR_HAS)) {
      queryParams.getPredicates(OPERATOR_HAS).forEach(has => {
        switch (has.field) {
          case PREDICATE_ATTACHMENT:
            selector.$and.push({
              _id: {
                $in: Attachments.find({}, { fields: { cardId: 1 } }).map(
                  a => a.cardId,
                ),
              },
            });
            break;
          case PREDICATE_CHECKLIST:
            selector.$and.push({
              _id: {
                $in: Checklists.find({}, { fields: { cardId: 1 } }).map(
                  a => a.cardId,
                ),
              },
            });
            break;
          case PREDICATE_DESCRIPTION:
          case PREDICATE_START_AT:
          case PREDICATE_DUE_AT:
          case PREDICATE_END_AT:
            if (has.exists) {
              selector[has.field] = { $exists: true, $nin: [null, ''] };
            } else {
              selector[has.field] = { $in: [null, ''] };
            }
            break;
          case PREDICATE_ASSIGNEES:
          case PREDICATE_MEMBERS:
            if (has.exists) {
              selector[has.field] = { $exists: true, $nin: [null, []] };
            } else {
              selector[has.field] = { $in: [null, []] };
            }
            break;
        }
      });
    }

    if (queryParams.text) {
      const regex = new RegExp(escapeForRegex(queryParams.text), 'i');

      const items = ChecklistItems.find(
        { title: regex },
        { fields: { cardId: 1 } },
      );
      const checklists = Checklists.find(
        {
          $or: [
            { title: regex },
            { _id: { $in: items.map(item => item.checklistId) } },
          ],
        },
        { fields: { cardId: 1 } },
      );

      const attachments = Attachments.find({ 'original.name': regex });

      const comments = CardComments.find(
        { text: regex },
        { fields: { cardId: 1 } },
      );

      selector.$and.push({
        $or: [
          { title: regex },
          { description: regex },
          { customFields: { $elemMatch: { value: regex } } },
          // {
          //   _id: {
          //     $in: CardComments.textSearch(userId, [queryParams.text]).map(
          //       com => com.cardId,
          //     ),
          //   },
          // },
          { _id: { $in: checklists.map(list => list.cardId) } },
          { _id: { $in: attachments.map(attach => attach.cardId) } },
          { _id: { $in: comments.map(com => com.cardId) } },
        ],
      });
    }

    if (selector.$and.length === 0) {
      delete selector.$and;
    }
  }

  // eslint-disable-next-line no-console
  // console.log('selector:', selector);
  // eslint-disable-next-line no-console
  // console.log('selector.$and:', selector.$and);

  const query = new Query();
  query.selector = selector;
  query.params = queryParams;
  query._errors = errors;

  return query;
}

function buildProjection(query) {
  let skip = 0;
  if (query.params.skip) {
    skip = query.params.skip;
  }
  let limit = DEFAULT_LIMIT;
  const configLimit = parseInt(process.env.RESULTS_PER_PAGE, 10);
  if (!isNaN(configLimit) && configLimit > 0) {
    limit = configLimit;
  }

  if (query.params.hasOperator(OPERATOR_LIMIT)) {
    limit = query.params.getPredicate(OPERATOR_LIMIT);
  }

  const projection = {
    fields: {
      _id: 1,
      archived: 1,
      boardId: 1,
      swimlaneId: 1,
      listId: 1,
      title: 1,
      type: 1,
      sort: 1,
      members: 1,
      assignees: 1,
      colors: 1,
      dueAt: 1,
      createdAt: 1,
      modifiedAt: 1,
      labelIds: 1,
      customFields: 1,
    },
    sort: {
      boardId: 1,
      swimlaneId: 1,
      listId: 1,
      sort: 1,
    },
    skip,
    limit,
  };

  if (query.params.hasOperator(OPERATOR_SORT)) {
    const order =
      query.params.getPredicate(OPERATOR_SORT).order === ORDER_ASCENDING
        ? 1
        : -1;
    switch (query.params.getPredicate(OPERATOR_SORT).name) {
      case PREDICATE_DUE_AT:
        projection.sort = {
          dueAt: order,
          boardId: 1,
          swimlaneId: 1,
          listId: 1,
          sort: 1,
        };
        break;
      case PREDICATE_MODIFIED_AT:
        projection.sort = {
          modifiedAt: order,
          boardId: 1,
          swimlaneId: 1,
          listId: 1,
          sort: 1,
        };
        break;
      case PREDICATE_CREATED_AT:
        projection.sort = {
          createdAt: order,
          boardId: 1,
          swimlaneId: 1,
          listId: 1,
          sort: 1,
        };
        break;
      case PREDICATE_SYSTEM:
        projection.sort = {
          boardId: order,
          swimlaneId: order,
          listId: order,
          modifiedAt: order,
          sort: order,
        };
        break;
    }
  }

  // eslint-disable-next-line no-console
  // console.log('projection:', projection);

  query.projection = projection;

  return query;
}

function buildQuery(queryParams) {
  const query = buildSelector(queryParams);

  return buildProjection(query);
}

Meteor.publish('brokenCards', function(sessionId) {
  check(sessionId, String);

  const params = new QueryParams();
  params.addPredicate(OPERATOR_STATUS, PREDICATE_ALL);
  const query = buildQuery(params);
  query.selector.$or = [
    { boardId: { $in: [null, ''] } },
    { swimlaneId: { $in: [null, ''] } },
    { listId: { $in: [null, ''] } },
  ];
  // console.log('brokenCards selector:', query.selector);

  return findCards(sessionId, query);
});

Meteor.publish('nextPage', function(sessionId) {
  check(sessionId, String);

  const session = SessionData.findOne({ sessionId });
  const projection = session.getProjection();
  projection.skip = session.lastHit;

  return findCards(sessionId, new Query(session.getSelector(), projection));
});

Meteor.publish('previousPage', function(sessionId) {
  check(sessionId, String);

  const session = SessionData.findOne({ sessionId });
  const projection = session.getProjection();
  projection.skip = session.lastHit - session.resultsCount - projection.limit;

  return findCards(sessionId, new Query(session.getSelector(), projection));
});

function findCards(sessionId, query) {
  const userId = Meteor.userId();

  // eslint-disable-next-line no-console
  // console.log('selector:', query.selector);
  // console.log('selector.$and:', query.selector.$and);
  // eslint-disable-next-line no-console
  // console.log('projection:', projection);
  let cards;
  if (!query.hasErrors()) {
    cards = Cards.find(query.selector, query.projection);
  }
  // eslint-disable-next-line no-console
  // console.log('count:', cards.count());

  const update = {
    $set: {
      totalHits: 0,
      lastHit: 0,
      resultsCount: 0,
      cards: [],
      selector: SessionData.pickle(query.selector),
      projection: SessionData.pickle(query.projection),
      errors: query.errors(),
    },
  };

  if (cards) {
    update.$set.totalHits = cards.count();
    update.$set.lastHit =
      query.projection.skip + query.projection.limit < cards.count()
        ? query.projection.skip + query.projection.limit
        : cards.count();
    update.$set.cards = cards.map(card => {
      return card._id;
    });
    update.$set.resultsCount = update.$set.cards.length;
  }

  SessionData.upsert({ userId, sessionId }, update);

  // remove old session data
  SessionData.remove({
    userId,
    modifiedAt: {
      $lt: new Date(
        moment()
          .subtract(1, 'day')
          .format(),
      ),
    },
  });

  if (cards) {
    const boards = [];
    const swimlanes = [];
    const lists = [];
    const customFieldIds = [];
    const users = [this.userId];

    cards.forEach(card => {
      if (card.boardId) boards.push(card.boardId);
      if (card.swimlaneId) swimlanes.push(card.swimlaneId);
      if (card.listId) lists.push(card.listId);
      if (card.members) {
        card.members.forEach(userId => {
          users.push(userId);
        });
      }
      if (card.assignees) {
        card.assignees.forEach(userId => {
          users.push(userId);
        });
      }
      if (card.customFields) {
        card.customFields.forEach(field => {
          customFieldIds.push(field._id);
        });
      }
    });

    const fields = {
      _id: 1,
      title: 1,
      archived: 1,
      sort: 1,
      type: 1,
    };

    return [
      cards,
      Boards.find(
        { _id: { $in: boards } },
        { fields: { ...fields, labels: 1, color: 1 } },
      ),
      Swimlanes.find(
        { _id: { $in: swimlanes } },
        { fields: { ...fields, color: 1 } },
      ),
      Lists.find({ _id: { $in: lists } }, { fields }),
      CustomFields.find({ _id: { $in: customFieldIds } }),
      Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
      Checklists.find({ cardId: { $in: cards.map(c => c._id) } }),
      Attachments.find({ cardId: { $in: cards.map(c => c._id) } }),
      CardComments.find({ cardId: { $in: cards.map(c => c._id) } }),
      SessionData.find({ userId, sessionId }),
    ];
  }

  return [SessionData.find({ userId, sessionId })];
}
