'use strict';
const Promise = require('bluebird');
const lb = require('loopback');
const scheduler = require('../../server/scripts/spot-scheduler');
const {notify} = require('../../server/scripts/notification-module');

module.exports = function(Spot) {
  Spot.addSpot = (req, spot, cb) => {
    const senderId = req.accessToken.userId;
    const tags = spot.tags;
    const visibleBy = spot.visibleBy;
    const duration = spot.isPublic ? spot.duration : 30;
    spot.senderId = senderId;

    Promise.resolve(spot)
      .tap(ensureSpotIntegrity)
      .then(spot => setSpotExpirationDate(spot, duration))
      .then(() => new Promise((resolve, reject) =>
        Spot.create(spot, (err, s) => err ? reject(err) : resolve(s))
      ))
      .then(createdSpot => addTagsToSpot(createdSpot, tags))
      .tap(createdSpot => !createdSpot.isPublic &&
        addVisibilityToSpot(createdSpot, visibleBy))
      .tap(spotAction)
      .tap(spotNotification)
      .then(createdSpot => cb(null, createdSpot))
      .catch(cb);
  };

  Spot.getVisibleSpots = (req, latlng, cb) => {
    const currentUserId = req.accessToken ? req.accessToken.userId : null;

    Spot.app.models.Member.updateLastGeolocation(currentUserId, latlng);
    Spot.app.models.Member
      .findById(currentUserId, {include: [
        {relation: 'visibleSpots', scope: {where: {'isActive': true}}},
        {
          relation: 'spots',
          scope: {include: 'sender', where: {'isActive': true}},
        },
      ]})
      .then(member => member.visibleSpots().concat(member.spots()))
      .catch(() => [])
      .then((personnalSpots) => Promise.props({
        personnal: personnalSpots,
        public: getVisiblePublicSpot(latlng),
      }))
      .then(spots => spots.personnal
        .concat(spots.public)
        .filter((it, idx, ar) => ar.findIndex(s => s.id === it.id) === idx))
      .then((allSpots) => cb(null, allSpots));
  };

  Spot.respot = (req, spotId, cb) => {
    const currentUserId = req.accessToken.userId;

    Spot.app.models.Member.findById(currentUserId)
      .then(sender => Promise.props({
        sender,
        senderFriends: Spot.app.models.Friend.getFriends(currentUserId),
        spot: Spot.findById(spotId, {include: 'sender'}),
      }))
      .tap(({sender, senderFriends, spot}) => {
        if (spot.isPublic) {
          addVisibilityToSpot(
            spot,
            senderFriends.map(f => f.id).concat(sender.id)
          );
        } else {
          extendSpotLifespan(spot, 30);
        }
        cb(null, spot);
      })
      .tap(respotAction.bind(Spot))
      .tap(respotNotification)
      .tap(({spot}) => cb(null, spot))
      .catch(cb);
  };

  Spot.prototype.expire = function() {
    return this.tags
      .find()
      .then(spotTags =>
        Promise.all(spotTags.map(t => t.updateAttribute('score', t.score - 1)))
      )
      .then(() => this.updateAttribute('isActive', false));
  };

  Spot.updateMarker = function(ctx, spotId, cb) {
    const Marker = lb.getModel('Marker');
    const memberId = ctx.req.accessToken.userId;

    ensureIsSender(spotId, memberId)
      .then(() => Marker.upload(ctx, 'spot-markers', undefined))
      .tap(() => Marker.destroyAll({spotId}))
      .map(marker => marker.updateAttribute('spotId', spotId))
      .then(data => cb(null, data[0]));
  };
};

function extendSpotLifespan(spot, duration) {
  setSpotExpirationDate(spot, duration);
  spot.save();
}

function getVisiblePublicSpot(latlng) {
  const Spot = lb.getModel('Spot');
  return Spot.find({
    where: {isPublic: true, isActive: true},
    include: ['sender'],
  })
    .then(spots => spots
      .filter(spot =>
        isInVisibleRange(latlng, spot.latlng, spot.range)
      )
    );
}

function isInVisibleRange(
  {lat: lat1, lng: lng1},
  {lat: lat2, lng: lng2},
  maxDistance
) {
  let R = 6371;
  let dLat = deg2rad(lat2 - lat1);
  let dLon = deg2rad(lng2 - lng1);
  let a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
    Math.sin(dLon / 2) * Math.sin(dLon / 2)
    ;
  let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  let d = R * c * 1000;
  return d > maxDistance ? false : true;
}

function deg2rad(deg) {
  return deg * (Math.PI / 180);
}

function ensureSpotIntegrity(spot) {
  const integrityTests = [
    {
      test: (s) => (!s.tags || !s.tags.length),
      getError: () => new Error('Aucun tag n\'est associé au spot'),
    },
    {
      test: (s) => (!s.isPublic && (!s.visibleBy || !s.visibleBy.length)),
      getError: () => new Error('Aucun amis n\'est associé au spot privé'),
    },
    {
      test: (s) => (s.public && !s.range || !s.duration),
      getError: () => new Error('Durée et rayon d\'action non définie'),
    },
  ];

  const passing = integrityTests.find(t => t.test(spot));
  if (passing) throw passing.getError(spot);
}

function addTagsToSpot(spot, tags) {
  const Tag = lb.getModel('Tag');
  const Spot = lb.getModel('Spot');
  const findOrcreateTags = tags.map(t =>
    Tag.findOrCreate({where: {name: t}}, {name: t})
  );

  return Promise.all(findOrcreateTags)
    .then(nested => nested.reduce((acc, it) => acc.concat(it[0]), []))
    .tap(allTags => Promise.all(allTags.map(t => spot.tags.add(t))))
    .tap(allTags =>
      Promise.all(allTags.map(t => t.updateAttribute('score', t.score + 1)))
    )
    .tap(allTags => addTagsToMember(spot.senderId, allTags))
    .then(() => Spot.findById(spot.id, {include: 'tags'}));
}

function addTagsToMember(memberId, tags) {
  const MemberTagScore = lb.getModel('MemberTagScore');
  const findOrcreateTagsScore = tags.map(t =>
    MemberTagScore.findOrCreate(
      {where: {memberId, tagId: t.name}},
      {memberId, tagId: t.name}
    )
  );

  return Promise.all(findOrcreateTagsScore)
    .then(nested => nested.reduce((acc, it) => acc.concat(it[0]), []))
    .then(allTags =>
      allTags.map(t => t.updateAttribute('score', t.score + 1))
    );
}

function setSpotExpirationDate(spot, duration) {
  const expirationDate = new Date();
  expirationDate.setTime(expirationDate.getTime() + duration * 60000);
  spot.expiration = expirationDate;
  scheduler.scheduleSpotExpiration(spot);
  return spot;
}

function addVisibilityToSpot(spot, visibleBy) {
  const Member = lb.getModel('Member');
  return Member.find({where: {id: {inq: visibleBy}}})
    .then(vb => vb.map((x) => spot.visibleBy.add(x)))
    .then(Promise.all);
}

function spotAction(spot) {
  const Action = lb.getModel('Action');
  const Member = lb.getModel('Member');

  return Member.findById(spot.senderId)
    .then((sender) => Action.create({
      name: `Spot ${spot.isPublic ? 'public' : 'privé'}`,
      description: `${sender.username} a envoyé un spot ` +
        `${spot.isPublic ? 'public' : 'privé'}`,
      authorId: sender.id,
      spotId: spot.id,
    }));
}

function respotAction({sender, senderFriends, spot}) {
  const Action = lb.getModel('Action');
  const originalSender = spot.sender();

  return Action.create({
    name: 'Respot',
    description: `${sender.username} a respot ` +
      `le spot ${spot.isPublic ? 'public' : 'privé'} ` +
      `de ${originalSender.username}`,
    data: {visibleBy: senderFriends.map(f => f.id)},
    authorId: sender.id,
    spotId: spot.id,
  });
}

function spotNotification(spot) {
  const Member = lb.getModel('Member');
  const Friend = lb.getModel('Friend');
  const type = spot.isPublic ?
    'PUBLIC_SPOT_SENT_BY_A_FRIEND' :
    'PRIVATE_SPOT_RECEIVED';
  const recipients = spot.isPublic ?
    Friend.getFriends(spot.senderId) :
    spot.visibleBy.map(Member.findById);

  return Member.findById(spot.senderId)
    .then(sender => notify({
      type,
      sender: Member.findById(spot.senderId),
      data: {spot},
    }, recipients));
}

function respotNotification({sender, senderFriends, spot}) {
  const Member = lb.getModel('Member');
  const Friend = lb.getModel('Friend');
  const originalSender = spot.sender();

  return Promise.all([
    notify({
      type: 'PERSONNAL_SPOT_RESPOTTED',
      sender,
      data: {spot},
    }, originalSender),
    notify({
      type: 'PUBLIC_SPOT_RESPOTTED_BY_A_FRIEND',
      sender,
      data: {spot},
    }, senderFriends),
  ]);
}

function ensureIsSender(spotId, senderId) {
  const Spot = lb.getModel('Spot');

  return Spot.findById(spotId)
    .then(spot => spot.senderId !== senderId ? Promise.reject(spot) : spot);
}
