/*
 * Copyright (c) 2017, Hugo Freire <hugo@exec.sh>.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

const AWS_REGION = process.env.AWS_REGION
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY
const AWS_S3_BUCKET = process.env.AWS_S3_BUCKET
const AWS_REKOGNITION_COLLECTION = process.env.AWS_REKOGNITION_COLLECTION || 'get-me-a-tinder-date'

const _ = require('lodash')
const Promise = require('bluebird')

const Logger = require('modern-logger')

const S3 = require('./utils/s3')
const Rekognition = require('./utils/rekognition')

const request = Promise.promisifyAll(require('request').defaults({ encoding: null }))

const { parse } = require('url')

const deleteFaces = function (faces) {
  return this.rekognition.deleteFaces(AWS_REKOGNITION_COLLECTION, faces)
    .then(() => faces.length)
    .catch((error) => Logger.warn(error))
}

const indexFaces = function (images) {
  let indexedFaces = 0

  return Promise.map(images, (image) => {
    return this.rekognition.indexFaces(AWS_REKOGNITION_COLLECTION, AWS_S3_BUCKET, image)
      .then((data) => {
        if (!data.FaceRecords || _.isEmpty(data.FaceRecords)) {
          return
        }

        // delete images with no or multiple faces
        if (data.FaceRecords.length !== 1) {
          return this.rekognition.deleteFaces(AWS_REKOGNITION_COLLECTION, _.map(data.FaceRecords, ({ Face }) => Face.FaceId))
            .then(() => this.s3.deleteObject(image))
        }

        indexedFaces++
      })
      .catch((error) => {
        return this.s3.deleteObject(image)
          .then(() => Logger.warn(error))
      })
  }, { concurrency: 5 })
    .then(() => indexedFaces)
}

const savePhoto = function (photo) {
  if (!photo) {
    return Promise.reject(new Error('invalid arguments'))
  }

  const url = parse(photo.url)
  if (!url) {
    return Promise.reject(new Error('invalid photo url'))
  }

  return request.getAsync(url.href)
    .then(({ body }) => {
      return this.s3.putObject(`photos/tinder${url.pathname}`, body)
        .then(() => {
          photo.url = `https://s3-${AWS_REGION}.amazonaws.com/${AWS_S3_BUCKET}/photos/tinder${url.pathname}`

          return body
        })
    })
}

class Taste {
  constructor () {
    this.rekognition = new Rekognition({
      region: AWS_REGION,
      accessKeyId: AWS_ACCESS_KEY_ID,
      secretAccessKey: AWS_SECRET_ACCESS_KEY
    })

    this.s3 = new S3({
      region: AWS_REGION,
      bucket: AWS_S3_BUCKET,
      accessKeyId: AWS_ACCESS_KEY_ID,
      secretAccessKey: AWS_SECRET_ACCESS_KEY
    })
  }

  bootstrap () {
    return this.createRekognitionCollectionIfNeeded()
      .then(() => this.syncS3BucketAndRekognitionCollection())
  }

  createRekognitionCollectionIfNeeded () {
    return this.rekognition.listCollections()
      .then((collectionIds) => {
        if (!_.includes(collectionIds, AWS_REKOGNITION_COLLECTION)) {
          return this.rekognition.createCollection(AWS_REKOGNITION_COLLECTION)
        }
      })
  }

  syncS3BucketAndRekognitionCollection () {
    const start = _.now()

    return Promise.props({
      currentFaces: this.rekognition.listFaces(AWS_REKOGNITION_COLLECTION),
      availableImages: this.s3.listObjects('train')
    })
      .then(({ currentFaces, availableImages }) => {
        const { Faces } = currentFaces
        const currentImages = _(Faces)
          .map(({ ExternalImageId }) => ExternalImageId)
          .uniq()
          .value()

        const imagesToDelete = _.difference(currentImages, availableImages)
        const imagesToIndex = _.difference(availableImages, currentImages)

        // TODO: optimize the code below
        const facesToDelete = []
        _.forEach(imagesToDelete, (externalImageId) => {
          const images = _.filter(Faces, { ExternalImageId: externalImageId })

          _.forEach(images, ({ FaceId }) => {
            facesToDelete.push(FaceId)
          })
        })

        return Promise.props({
          deletedFaces: deleteFaces.bind(this)(imagesToDelete),
          indexedFaces: indexFaces.bind(this)(imagesToIndex)
        })
          .then(({ deletedFaces, indexedFaces }) => {
            const stop = _.now()
            const duration = _.round((stop - start) / 1000)

            return Logger.debug(`Synced reference face collection: ${Faces.length - deletedFaces + indexedFaces} faces available (time = ${duration}s, deleted = ${deletedFaces}, indexed = ${indexedFaces})`)
          })
      })
  }

  checkPhotosOut (photos) {
    const thumbnail = _.find(photos[ 0 ].processedFiles, { width: 84, height: 84 })
    return savePhoto.bind(this)(thumbnail)
      .then(() => {
        photos[ 0 ].thumbnailUrl = thumbnail.url

        return Promise.map(photos, (photo) => {
          return savePhoto.bind(this)(photo)
            .then((image) => this.rekognition.searchFacesByImage(AWS_REKOGNITION_COLLECTION, image))
            .then(({ FaceMatches }) => {
              photo.similarity = _.round(_.max(_.map(FaceMatches, 'Similarity')), 2) || 0

              return photo.similarity
            })
            .catch(() => {
              photo.similarity = 0

              return photo.similarity
            })
        }, { concurrency: 3 })
          .then((faceSimilarities) => {
            const faceSimilarityMax = _.max(faceSimilarities)
            const faceSimilarityMin = _.min(faceSimilarities)
            const faceSimilarityMean = _.round(_.mean(_.without(faceSimilarities, 0, undefined)), 2) || 0

            const like = !_.isEmpty(faceSimilarities) && faceSimilarityMean > 80

            return { faceSimilarities, faceSimilarityMax, faceSimilarityMin, faceSimilarityMean, like }
          })
      })
  }

  mentalSnapshot (photos) {
    return Promise.map(photos, (photo) => {
      const url = parse(photo.url)
      if (!url) {
        return
      }

      const srcKey = url.pathname
      const dstKey = srcKey.replace(`/${AWS_S3_BUCKET}/photos`, 'train')

      return this.s3.copyObject(srcKey, dstKey)
        .then(() => dstKey)
    }, { concurrency: 5 })
      .then((images) => {
        return indexFaces.bind(this)(images)
          .then((indexedImages) => Logger.debug(`Indexed ${indexedImages} face(s)`))
      })
  }
}

module.exports = new Taste()

@@ -1238,39 +1238,8 @@
 ords
- %7C%7C _.isEmpty(data.FaceRecords)
 ) %7B%0A
@@ -1507,24 +1507,47 @@
  .then(() =%3E
+ %7B%0A              return
  this.s3.del
@@ -1558,24 +1558,38 @@
 bject(image)
+%0A            %7D
 )%0A        %7D%0A

remove from s3 photos that do not contain faces