const DateString = Match.Where(function(dateAsString) {
  check(dateAsString, String);
  return moment(dateAsString, moment.ISO_8601).isValid();
});

export class TrelloCreator {
  constructor(data) {
    // we log current date, to use the same timestamp for all our actions.
    // this helps to retrieve all elements performed by the same import.
    this._nowDate = new Date();
    // The object creation dates, indexed by Trello id
    // (so we only parse actions once!)
    this.createdAt = {
      board: null,
      cards: {},
      lists: {},
    };
    // The object creator Trello Id, indexed by the object Trello id
    // (so we only parse actions once!)
    this.createdBy = {
      cards: {}, // only cards have a field for that
    };

    // Map of labels Trello ID => Wekan ID
    this.labels = {};
    // Default swimlane
    this.swimlane = null;
    // Map of lists Trello ID => Wekan ID
    this.lists = {};
    // Map of cards Trello ID => Wekan ID
    this.cards = {};
    // Map of attachments Wekan ID => Wekan ID
    this.attachmentIds = {};
    // Map of checklists Wekan ID => Wekan ID
    this.checklists = {};
    // The comments, indexed by Trello card id (to map when importing cards)
    this.comments = {};
    // the members, indexed by Trello member id => Wekan user ID
    this.members = data.membersMapping ? data.membersMapping : {};

    // maps a trelloCardId to an array of trelloAttachments
    this.attachments = {};
  }

  /**
   * If dateString is provided,
   * return the Date it represents.
   * If not, will return the date when it was first called.
   * This is useful for us, as we want all import operations to
   * have the exact same date for easier later retrieval.
   *
   * @param {String} dateString a properly formatted Date
   */
  _now(dateString) {
    if (dateString) {
      return new Date(dateString);
    }
    if (!this._nowDate) {
      this._nowDate = new Date();
    }
    return this._nowDate;
  }

  /**
   * if trelloUserId is provided and we have a mapping,
   * return it.
   * Otherwise return current logged user.
   * @param trelloUserId
   * @private
   */
  _user(trelloUserId) {
    if (trelloUserId && this.members[trelloUserId]) {
      return this.members[trelloUserId];
    }
    return Meteor.userId();
  }

  checkActions(trelloActions) {
    check(trelloActions, [
      Match.ObjectIncluding({
        data: Object,
        date: DateString,
        type: String,
      }),
    ]);
    // XXX we could perform more thorough checks based on action type
  }

  checkBoard(trelloBoard) {
    check(
      trelloBoard,
      Match.ObjectIncluding({
        closed: Boolean,
        name: String,
        prefs: Match.ObjectIncluding({
          // XXX refine control by validating 'background' against a list of
          // allowed values (is it worth the maintenance?)
          background: String,
          permissionLevel: Match.Where(value => {
            return ['org', 'private', 'public'].indexOf(value) >= 0;
          }),
        }),
      }),
    );
  }

  checkCards(trelloCards) {
    check(trelloCards, [
      Match.ObjectIncluding({
        closed: Boolean,
        dateLastActivity: DateString,
        desc: String,
        idLabels: [String],
        idMembers: [String],
        name: String,
        pos: Number,
      }),
    ]);
  }

  checkLabels(trelloLabels) {
    check(trelloLabels, [
      Match.ObjectIncluding({
        // XXX refine control by validating 'color' against a list of allowed
        // values (is it worth the maintenance?)
        name: String,
      }),
    ]);
  }

  checkLists(trelloLists) {
    check(trelloLists, [
      Match.ObjectIncluding({
        closed: Boolean,
        name: String,
      }),
    ]);
  }

  checkChecklists(trelloChecklists) {
    check(trelloChecklists, [
      Match.ObjectIncluding({
        idBoard: String,
        idCard: String,
        name: String,
        checkItems: [
          Match.ObjectIncluding({
            state: String,
            name: String,
          }),
        ],
      }),
    ]);
  }

  // You must call parseActions before calling this one.
  createBoardAndLabels(trelloBoard) {
    const boardToCreate = {
      archived: trelloBoard.closed,
      color: this.getColor(trelloBoard.prefs.background),
      // very old boards won't have a creation activity so no creation date
      createdAt: this._now(this.createdAt.board),
      labels: [],
      members: [
        {
          userId: Meteor.userId(),
          isAdmin: true,
          isActive: true,
          isNoComments: false,
          isCommentOnly: false,
          swimlaneId: false,
        },
      ],
      permission: this.getPermission(trelloBoard.prefs.permissionLevel),
      slug: getSlug(trelloBoard.name) || 'board',
      stars: 0,
      title: trelloBoard.name,
    };
    // now add other members
    if (trelloBoard.memberships) {
      trelloBoard.memberships.forEach(trelloMembership => {
        const trelloId = trelloMembership.idMember;
        // do we have a mapping?
        if (this.members[trelloId]) {
          const wekanId = this.members[trelloId];
          // do we already have it in our list?
          const wekanMember = boardToCreate.members.find(
            wekanMember => wekanMember.userId === wekanId,
          );
          if (wekanMember) {
            // we're already mapped, but maybe with lower rights
            if (!wekanMember.isAdmin) {
              wekanMember.isAdmin = this.getAdmin(trelloMembership.memberType);
            }
          } else {
            boardToCreate.members.push({
              userId: wekanId,
              isAdmin: this.getAdmin(trelloMembership.memberType),
              isActive: true,
              isNoComments: false,
              isCommentOnly: false,
              swimlaneId: false,
            });
          }
        }
      });
    }
    trelloBoard.labels.forEach(label => {
      const labelToCreate = {
        _id: Random.id(6),
        color: label.color ? label.color : 'black',
        name: label.name,
      };
      // We need to remember them by Trello ID, as this is the only ref we have
      // when importing cards.
      this.labels[label.id] = labelToCreate._id;
      boardToCreate.labels.push(labelToCreate);
    });
    const boardId = Boards.direct.insert(boardToCreate);
    Boards.direct.update(boardId, { $set: { modifiedAt: this._now() } });
    // log activity
    Activities.direct.insert({
      activityType: 'importBoard',
      boardId,
      createdAt: this._now(),
      source: {
        id: trelloBoard.id,
        system: 'Trello',
        url: trelloBoard.url,
      },
      // We attribute the import to current user,
      // not the author from the original object.
      userId: this._user(),
    });
    return boardId;
  }

  /**
   * Create the Wekan cards corresponding to the supplied Trello cards,
   * as well as all linked data: activities, comments, and attachments
   * @param trelloCards
   * @param boardId
   * @returns {Array}
   */
  createCards(trelloCards, boardId) {
    const result = [];
    trelloCards.forEach(card => {
      const cardToCreate = {
        archived: card.closed,
        boardId,
        // very old boards won't have a creation activity so no creation date
        createdAt: this._now(this.createdAt.cards[card.id]),
        dateLastActivity: this._now(),
        description: card.desc,
        listId: this.lists[card.idList],
        swimlaneId: this.swimlane,
        sort: card.pos,
        title: card.name,
        // we attribute the card to its creator if available
        userId: this._user(this.createdBy.cards[card.id]),
        dueAt: card.due ? this._now(card.due) : null,
      };
      // add labels
      if (card.idLabels) {
        cardToCreate.labelIds = card.idLabels.map(trelloId => {
          return this.labels[trelloId];
        });
      }
      // add members {
      if (card.idMembers) {
        const wekanMembers = [];
        // we can't just map, as some members may not have been mapped
        card.idMembers.forEach(trelloId => {
          if (this.members[trelloId]) {
            const wekanId = this.members[trelloId];
            // we may map multiple Trello members to the same wekan user
            // in which case we risk adding the same user multiple times
            if (!wekanMembers.find(wId => wId === wekanId)) {
              wekanMembers.push(wekanId);
            }
          }
          return true;
        });
        if (wekanMembers.length > 0) {
          cardToCreate.members = wekanMembers;
        }
      }
      // insert card
      const cardId = Cards.direct.insert(cardToCreate);
      // keep track of Trello id => Wekan id
      this.cards[card.id] = cardId;
      // log activity
      // Activities.direct.insert({
      //   activityType: 'importCard',
      //   boardId,
      //   cardId,
      //   createdAt: this._now(),
      //   listId: cardToCreate.listId,
      //   source: {
      //     id: card.id,
      //     system: 'Trello',
      //     url: card.url,
      //   },
      //   // we attribute the import to current user,
      //   // not the author of the original card
      //   userId: this._user(),
      // });
      // add comments
      const comments = this.comments[card.id];
      if (comments) {
        comments.forEach(comment => {
          const commentToCreate = {
            boardId,
            cardId,
            createdAt: this._now(comment.date),
            text: comment.data.text,
            // we attribute the comment to the original author, default to current user
            userId: this._user(comment.idMemberCreator),
          };
          // dateLastActivity will be set from activity insert, no need to
          // update it ourselves
          const commentId = CardComments.direct.insert(commentToCreate);
          // We need to keep adding comment activities this way with Trello
          // because it doesn't provide a comment ID
          Activities.direct.insert({
            activityType: 'addComment',
            boardId: commentToCreate.boardId,
            cardId: commentToCreate.cardId,
            commentId,
            createdAt: this._now(comment.date),
            // we attribute the addComment (not the import)
            // to the original author - it is needed by some UI elements.
            userId: commentToCreate.userId,
          });
        });
      }
      const attachments = this.attachments[card.id];
      const trelloCoverId = card.idAttachmentCover;
      if (attachments) {
        attachments.forEach(att => {
          const file = new FS.File();
          // Simulating file.attachData on the client generates multiple errors
          // - HEAD returns null, which causes exception down the line
          // - the template then tries to display the url to the attachment which causes other errors
          // so we make it server only, and let UI catch up once it is done, forget about latency comp.
          const self = this;
          if (Meteor.isServer) {
            file.attachData(att.url, function(error) {
              file.boardId = boardId;
              file.cardId = cardId;
              file.userId = self._user(att.idMemberCreator);
              // The field source will only be used to prevent adding
              // attachments' related activities automatically
              file.source = 'import';
              if (error) {
                throw error;
              } else {
                const wekanAtt = Attachments.insert(file, () => {
                  // we do nothing
                });
                self.attachmentIds[att.id] = wekanAtt._id;
                //
                if (trelloCoverId === att.id) {
                  Cards.direct.update(cardId, {
                    $set: { coverId: wekanAtt._id },
                  });
                }
              }
            });
          }
          // todo XXX set cover - if need be
        });
      }
      result.push(cardId);
    });
    return result;
  }

  // Create labels if they do not exist and load this.labels.
  createLabels(trelloLabels, board) {
    trelloLabels.forEach(label => {
      const color = label.color;
      const name = label.name;
      const existingLabel = board.getLabel(name, color);
      if (existingLabel) {
        this.labels[label.id] = existingLabel._id;
      } else {
        const idLabelCreated = board.pushLabel(name, color);
        this.labels[label.id] = idLabelCreated;
      }
    });
  }

  createLists(trelloLists, boardId) {
    trelloLists.forEach(list => {
      const listToCreate = {
        archived: list.closed,
        boardId,
        // We are being defensing here by providing a default date (now) if the
        // creation date wasn't found on the action log. This happen on old
        // Trello boards (eg from 2013) that didn't log the 'createList' action
        // we require.
        createdAt: this._now(this.createdAt.lists[list.id]),
        title: list.name,
        sort: list.pos,
      };
      const listId = Lists.direct.insert(listToCreate);
      Lists.direct.update(listId, { $set: { updatedAt: this._now() } });
      this.lists[list.id] = listId;
      // log activity
      // Activities.direct.insert({
      //   activityType: 'importList',
      //   boardId,
      //   createdAt: this._now(),
      //   listId,
      //   source: {
      //     id: list.id,
      //     system: 'Trello',
      //   },
      //   // We attribute the import to current user,
      //   // not the creator of the original object
      //   userId: this._user(),
      // });
    });
  }

  createSwimlanes(boardId) {
    const swimlaneToCreate = {
      archived: false,
      boardId,
      // We are being defensing here by providing a default date (now) if the
      // creation date wasn't found on the action log. This happen on old
      // Wekan boards (eg from 2013) that didn't log the 'createList' action
      // we require.
      createdAt: this._now(),
      title: 'Default',
      sort: 1,
    };
    const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
    Swimlanes.direct.update(swimlaneId, { $set: { updatedAt: this._now() } });
    this.swimlane = swimlaneId;
  }

  createChecklists(trelloChecklists) {
    trelloChecklists.forEach(checklist => {
      if (this.cards[checklist.idCard]) {
        // Create the checklist
        const checklistToCreate = {
          cardId: this.cards[checklist.idCard],
          title: checklist.name,
          createdAt: this._now(),
          sort: checklist.pos,
        };
        const checklistId = Checklists.direct.insert(checklistToCreate);
        // keep track of Trello id => Wekan id
        this.checklists[checklist.id] = checklistId;
        // Now add the items to the checklistItems
        let counter = 0;
        checklist.checkItems.forEach(item => {
          counter++;
          const checklistItemTocreate = {
            _id: checklistId + counter,
            title: item.name,
            checklistId: this.checklists[checklist.id],
            cardId: this.cards[checklist.idCard],
            sort: item.pos,
            isFinished: item.state === 'complete',
          };
          ChecklistItems.direct.insert(checklistItemTocreate);
        });
      }
    });
  }

  getAdmin(trelloMemberType) {
    return trelloMemberType === 'admin';
  }

  getColor(trelloColorCode) {
    // trello color name => wekan color
    const mapColors = {
      blue: 'belize',
      orange: 'pumpkin',
      green: 'nephritis',
      red: 'pomegranate',
      purple: 'wisteria',
      pink: 'moderatepink',
      lime: 'limegreen',
      sky: 'strongcyan',
      grey: 'midnight',
    };
    const wekanColor = mapColors[trelloColorCode];
    return wekanColor || Boards.simpleSchema()._schema.color.allowedValues[0];
  }

  getPermission(trelloPermissionCode) {
    if (trelloPermissionCode === 'public') {
      return 'public';
    }
    // Wekan does NOT have organization level, so we default both 'private' and
    // 'org' to private.
    return 'private';
  }

  parseActions(trelloActions) {
    trelloActions.forEach(action => {
      if (action.type === 'addAttachmentToCard') {
        // We have to be cautious, because the attachment could have been removed later.
        // In that case Trello still reports its addition, but removes its 'url' field.
        // So we test for that
        const trelloAttachment = action.data.attachment;
        // We need the idMemberCreator
        trelloAttachment.idMemberCreator = action.idMemberCreator;
        if (trelloAttachment.url) {
          // we cannot actually create the Wekan attachment, because we don't yet
          // have the cards to attach it to, so we store it in the instance variable.
          const trelloCardId = action.data.card.id;
          if (!this.attachments[trelloCardId]) {
            this.attachments[trelloCardId] = [];
          }
          this.attachments[trelloCardId].push(trelloAttachment);
        }
      } else if (action.type === 'commentCard') {
        const id = action.data.card.id;
        if (this.comments[id]) {
          this.comments[id].push(action);
        } else {
          this.comments[id] = [action];
        }
      } else if (action.type === 'createBoard') {
        this.createdAt.board = action.date;
      } else if (action.type === 'createCard') {
        const cardId = action.data.card.id;
        this.createdAt.cards[cardId] = action.date;
        this.createdBy.cards[cardId] = action.idMemberCreator;
      } else if (action.type === 'createList') {
        const listId = action.data.list.id;
        this.createdAt.lists[listId] = action.date;
      }
    });
  }

  importActions(actions, boardId) {
    actions.forEach(action => {
      switch (action.type) {
        // Board related actions
        // TODO: addBoardMember, removeBoardMember
        case 'createBoard': {
          Activities.direct.insert({
            userId: this._user(action.idMemberCreator),
            type: 'board',
            activityTypeId: boardId,
            activityType: 'createBoard',
            boardId,
            createdAt: this._now(action.date),
          });
          break;
        }
        // List related activities
        // TODO: removeList, archivedList
        case 'createList': {
          Activities.direct.insert({
            userId: this._user(action.idMemberCreator),
            type: 'list',
            activityType: 'createList',
            listId: this.lists[action.data.list.id],
            boardId,
            createdAt: this._now(action.date),
          });
          break;
        }
        // Card related activities
        // TODO: archivedCard, restoredCard, joinMember, unjoinMember
        case 'createCard': {
          Activities.direct.insert({
            userId: this._user(action.idMemberCreator),
            activityType: 'createCard',
            listId: this.lists[action.data.list.id],
            cardId: this.cards[action.data.card.id],
            boardId,
            createdAt: this._now(action.date),
          });
          break;
        }
        case 'updateCard': {
          if (action.data.old.idList) {
            Activities.direct.insert({
              userId: this._user(action.idMemberCreator),
              oldListId: this.lists[action.data.old.idList],
              activityType: 'moveCard',
              listId: this.lists[action.data.listAfter.id],
              cardId: this.cards[action.data.card.id],
              boardId,
              createdAt: this._now(action.date),
            });
          }
          break;
        }
        // Comment related activities
        // Trello doesn't export the comment id
        // Attachment related activities
        case 'addAttachmentToCard': {
          Activities.direct.insert({
            userId: this._user(action.idMemberCreator),
            type: 'card',
            activityType: 'addAttachment',
            attachmentId: this.attachmentIds[action.data.attachment.id],
            cardId: this.cards[action.data.card.id],
            boardId,
            createdAt: this._now(action.date),
          });
          break;
        }
        // Checklist related activities
        case 'addChecklistToCard': {
          Activities.direct.insert({
            userId: this._user(action.idMemberCreator),
            activityType: 'addChecklist',
            cardId: this.cards[action.data.card.id],
            checklistId: this.checklists[action.data.checklist.id],
            boardId,
            createdAt: this._now(action.date),
          });
          break;
        }
      }
      // Trello doesn't have an add checklist item action
    });
  }

  check(board) {
    try {
      // check(data, {
      //   membersMapping: Match.Optional(Object),
      // });
      this.checkActions(board.actions);
      this.checkBoard(board);
      this.checkLabels(board.labels);
      this.checkLists(board.lists);
      this.checkCards(board.cards);
      this.checkChecklists(board.checklists);
    } catch (e) {
      throw new Meteor.Error('error-json-schema');
    }
  }

  create(board, currentBoardId) {
    // TODO : Make isSandstorm variable global
    const isSandstorm =
      Meteor.settings &&
      Meteor.settings.public &&
      Meteor.settings.public.sandstorm;
    if (isSandstorm && currentBoardId) {
      const currentBoard = Boards.findOne(currentBoardId);
      currentBoard.archive();
    }
    this.parseActions(board.actions);
    const boardId = this.createBoardAndLabels(board);
    this.createLists(board.lists, boardId);
    this.createSwimlanes(boardId);
    this.createCards(board.cards, boardId);
    this.createChecklists(board.checklists);
    this.importActions(board.actions, boardId);
    // XXX add members
    return boardId;
  }
}
