/* @flow */

import S3 from 'aws-sdk/clients/s3'
import logger from 'winston'

import { BadPathError } from '../errors'
import type { ListFilesResult, PerformWriteArgs } from '../driverModel'
import { DriverStatics, DriverModel } from '../driverModel'

type S3_CONFIG_TYPE = { awsCredentials: {
                          accessKeyId?: string,
                          secretAccessKey?: string,
                          sessionToken?: string
                        },
                        pageSize?: number,
                        cacheControl?: string,
                        bucket: string }

class S3Driver implements DriverModel {
  s3: S3
  bucket: string
  pageSize: number
  cacheControl: ?string
  initPromise: Promise<void>

  static getConfigInformation() {
    const envVars = {}
    const awsCredentials = {}
    if (process.env['GAIA_S3_ACCESS_KEY_ID']) {
      awsCredentials['accessKeyId'] = process.env['GAIA_S3_ACCESS_KEY_ID']
      envVars['awsCredentials'] = awsCredentials
    }
    if (process.env['GAIA_S3_SECRET_ACCESS_KEY']) {
      awsCredentials['secretAccessKey'] = process.env['GAIA_S3_SECRET_ACCESS_KEY']
      envVars['awsCredentials'] = awsCredentials
    }
    if (process.env['GAIA_S3_SESSION_TOKEN']) {
      awsCredentials['sessionToken'] = process.env['GAIA_S3_SESSION_TOKEN']
      envVars['awsCredentials'] = awsCredentials
    }

    return {
      defaults: { awsCredentials: undefined },
      envVars
    }
  }

  constructor (config: S3_CONFIG_TYPE) {
    this.s3 = new S3(config.awsCredentials)
    this.bucket = config.bucket
    this.pageSize = config.pageSize ? config.pageSize : 100
    this.cacheControl = config.cacheControl
    this.initPromise = this.createIfNeeded()
  }

  ensureInitialized() {
    return this.initPromise
  }

  dispose() {
    return Promise.resolve()
  }

  static isPathValid(path: string){
    // for now, only disallow double dots.
    return (path.indexOf('..') === -1)
  }

  getReadURLPrefix(): string {
    return `https://${this.bucket}.s3.amazonaws.com/`
  }

  createIfNeeded () {
    return new Promise<void>((resolve, reject) => {
      this.s3.headBucket( { Bucket: this.bucket }, (error) => {
        if (error && error.code === 'NotFound') { // try to create
          const params = {
            Bucket: this.bucket,
            ACL: 'public-read'
          }
          this.s3.createBucket(params, (error) => {
            if (error) {
              logger.error(`failed to initialize s3 bucket: ${error}`)
              process.exit()
              reject(error)
            } else {
              logger.info(`initialized s3 bucket: ${this.bucket}`)
              resolve()
            }
          })
        } else if (error) {
          logger.error(`failed to connect to s3 bucket: ${error}`)
          process.exit()
          reject(error)
        } else {
          logger.info(`connected to s3 bucket: ${this.bucket}`)
          resolve()
        }
      })
    })
  }

  listAllKeys(prefix: string, page: ?string) : Promise<ListFilesResult> {
    // returns {'entries': [...], 'page': next_page}
    const opts : { Bucket: string, MaxKeys: number, Prefix: string, ContinuationToken?: string } = {
      Bucket: this.bucket,
      MaxKeys: this.pageSize,
      Prefix: prefix
    }
    if (page) {
      opts.ContinuationToken = page
    }
    return new Promise((resolve, reject) => {
      this.s3.listObjectsV2(opts, (err, data) => {
        if (err) {
          return reject(err)
        }
        const res : ListFilesResult = {
          entries: data.Contents.map((e) => e.Key.slice(prefix.length + 1)),
          page: data.IsTruncated ? data.NextContinuationToken : null
        }
        return resolve(res)
      })
    })
  }

  listFiles(prefix: string, page: ?string) {
    // returns {'entries': [...], 'page': next_page}
    return this.listAllKeys(prefix, page)
  }

  performWrite(args: PerformWriteArgs) : Promise<string> {
    const s3key = `${args.storageTopLevel}/${args.path}`
    const s3params = Object.assign({}, {
      Bucket: this.bucket,
      Key: s3key,
      Body: args.stream,
      ContentType: args.contentType,
      ACL: 'public-read'
    })
    if (this.cacheControl) {
      s3params.CacheControl = this.cacheControl
    }

    if (!S3Driver.isPathValid(args.path)){
      return Promise.reject(new BadPathError('Invalid Path'))
    }

    // Upload stream to s3
    return new Promise((resolve, reject) => {
      this.s3.upload(s3params, (err) => {
        if (err) {
          logger.error(`failed to store ${s3key} in bucket ${this.bucket}`)
          return reject(new Error('S3 storage failure: failed to store' +
                                  ` ${s3key} in bucket ${this.bucket}: ${err}`))
        }
        const publicURL = `${this.getReadURLPrefix()}${s3key}`
        logger.debug(`storing ${s3key} in bucket ${this.bucket}`)
        return resolve(publicURL)
      })
    })
  }
}

(S3Driver: DriverStatics)

export default S3Driver

@@ -3888,16 +3888,22 @@
 %0A  %7D%0A%0A  
+async 
 performW
@@ -4325,30 +4325,14 @@
     
-return Promise.reject(
+throw 
 new 
@@ -4359,17 +4359,16 @@
 d Path')
-)
 %0A    %7D%0A%0A
@@ -4394,30 +4394,41 @@
 m to s3%0A    
-return
+try %7B%0A      await
  new Promise
@@ -4444,32 +4444,24 @@
 , reject) =%3E
- %7B%0A     
  this.s3.upl
@@ -4478,22 +4478,23 @@
 ms, (err
+or
 ) =%3E 
-%7B
 %0A       
@@ -4490,39 +4490,89 @@
 =%3E %0A        
-if (err) %7B%0A    
+error ? reject(error) : resolve()%0A      ))%0A    %7D catch (error) %7B%0A
       logger
@@ -4637,34 +4637,22 @@
 )%0A      
-    return reject(
+throw 
 new Erro
@@ -4697,34 +4697,8 @@
 ' +%0A
-                          
     
@@ -4747,31 +4747,25 @@
 %7Berr
+or
 %7D%60)
-)
 %0A    
-  
-  
 %7D%0A
- 
+%0A
     
-   
 cons
@@ -4815,28 +4815,24 @@
 s3key%7D%60%0A    
-   
- 
 logger.debug
@@ -4873,28 +4873,24 @@
 s.bucket%7D%60)%0A
-    
     return r
@@ -4888,24 +4888,16 @@
  return 
-resolve(
 publicUR
@@ -4897,33 +4897,16 @@
 ublicURL
-)%0A      %7D)%0A    %7D)
 %0A  %7D%0A%7D%0A%0A

Fix edge case errors in S3 driver `performWrite` method not being propagated through the promise chain.