const Base = require('./base'),
  Faas = require('./faas'),
  Jsonic = require('jsonic'),
  { S3Client } = require('@aws-sdk/client-s3'),
  { SNSClient, PublishCommand } = require('@aws-sdk/client-sns'),
  Fs = require('fs'),
  Path = require('path'),
  _ = require('lodash'),
  S3ObjectReadWrite = require('./aws/s3-object-read-write')

class LambdaConfig extends Faas.FaasConfig {
  defaultValues() {
    return this.constructor.defaultsDeep({
      backup: {
        bucket: null,
        prefix: null,
        acl: 'private',
      },
      upload: {
        acl: 'public-read',
        metadata: {},
      },
      sns: {
        topicArn: null,
        defaultValues: {},
      }
    }, super.defaultValues())
  }

  mappingEnvs(envs) {
    super.mappingEnvs(envs)

    const mapping = {
      UPLOAD_ACL: 'upload.acl',
      DESTINATION_ACL: 'upload.acl',
      BACKUP_BUCKET: 'backup.bucket',
      BACKUP_PREFIX: 'backup.prefix',
      BACKUP_ACL: 'backup.acl',
      METADATA: (c, v) => c.set('upload.metadata', Jsonic(v)),
      SNS_TOPIC_ARN: 'sns.topicArn',
      SNS_DEFAULT_VALUES: (c, v) => c.set('sns.defaultValues', Jsonic(v)),
    }
    this.mapping(mapping, envs)
  }
}

class LambdaContext extends Faas.FaasContext {
  constructor(event, lambdaContext) {
    super({
      rawEvent: event,
      lambdaContext,
    })

    this.mergeRecord()
  }

  mergeRecord() {
    const bucket = this.s3Bucket(),
      key = this.s3Key()

    const location = `s3://${bucket}`

    // マージする記録値(record)
    const merging = {
      record: {
        location,
        fileSystemUri: location,
        relPath: key,
      }
    }
    merging.record.fileSystemUri = merging.record.location,

    this.merge(merging)
  }

  defaultValues() {
    return this.constructor.defaultsDeep({
      convert: {
        srcBuffer: null,
      },
      s3: {
        bucket: null,
        key: null,
        entry: null,
      },
      backup: {
        buffer: null,
        bucket: null,
        key: null,
      },
    }, super.defaultValues())
  }

  isS3BatchRequest() {
    const event = this.get('rawEvent') || {}
    return !!event.tasks
  }

  s3Bucket() {
    const event = this.get('rawEvent') || {}
    if (this.isS3BatchRequest()) {
      const arn = event.tasks[0].s3BucketArn
      return arn.split(/:::/).pop()
    } else {
      return event.Records[0].s3.bucket.name
    }
  }

  static decodeS3Key(key) {
    return decodeURIComponent(key).replace(/\+/g, ' ')
  }

  s3Key() {
    const event = this.get('rawEvent') || {}
    if (this.isS3BatchRequest()) {
      return this.constructor.decodeS3Key(event.tasks[0].s3Key)
    } else {
      return this.constructor.decodeS3Key(event.Records[0].s3.object.key)
    }
  }
}

class LambdaApp extends Faas.FaasApp {
  static get packageName() { return 'lambda' }

  get s3() {
    if (this._s3) return this._s3
    return this._s3 = new S3Client()
  }

  get sns() {
    if (this._sns) return this._sns
    return this._sns = new SNSClient()
  }

  locationParams() {
    return {
      Bucket: this.context.s3Bucket(),
      Key: this.context.s3Key(),
    }
  }

  async initContext(logger) {
    const functionName = this.context.get('lambdaContext.functionName', 'unknown'),
      functionVersion = this.context.get('lambdaContext.functionVersion', '0.0.0'),
      functionArn = this.context.get('lambdaContext.invokedFunctionArn', 'unknown'),
      memorySize = parseInt(this.context.get('lambdaContext.memoryLimitInMB', '0'))

    const serial = this.config.get('serial', ''),
      bucket = this.context.s3Bucket()
    const seed = [serial, functionArn, bucket, functionVersion, memorySize]
    const session = LambdaContext.sessionize(seed.join("\t"))
    const sessionLabel = `${functionArn}(version=${functionVersion},memory=${memorySize},bucket=${bucket})`

    this.context.merge({
      record: {
        charge: bucket,
        myhost: `lambda[${functionName}]`,
        memorySize,
        session,
        sessionLabel,
      }
    })
  }

  async downloadFile(logger) {
    const bucket = this.context.s3Bucket(),
      key = this.context.s3Key()

    // パスに基づく処理継続の監査
    await this.auditToProcess({ pathname: key })

    // S3オブジェクトエントリ
    const entry = new S3ObjectReadWrite(this.s3, bucket, key)

    // ヘッダ情報を最初に参照
    logger.trace(() => `ヘッダの取得を開始: s3://${bucket}/${key}`)
    entry.on('beforeHead', params => logger.debug(() => `S3.headObject(${logger.constructor.inspect(params)})`))
    await entry.head()
    logger.trace(() => `ヘッダの取得が完了: s3://${bucket}/${key}`)

    // ヘッダ情報に基づく処理継続の監査
    await this.auditToProcess({
      status: entry.metadata['lightfile-status'],
      size: entry.attributes.ContentLength,
    })

    // 内容をダウンロードして処理を開始
    logger.trace(() => `ダウンロードを開始: s3://${bucket}/${key}`)
    entry.on('beforeGet', params => logger.debug(() => `S3.getObject(${logger.constructor.inspect(params)})`))
    await entry.get()
    logger.trace(() => `ダウンロードが完了: s3://${bucket}/${key}`)

    // 次の工程にデータを引き渡し
    this.context.set('s3.entry', entry)
    this.context.set('convert.srcPath', key)
    this.context.set('convert.srcBuffer', entry.body)
  }

  static scripting(str, values) {
    if (!_.isString(str)) return ''
    const replaced = str.replace(/\$\{(.+?)\}/g, (all, name) => {
      return values[name] || name
    })
    return replaced
  }

  async backupFile(logger) {
    const backup = {
      bucket: this.config.get('backup.bucket'),
      prefix: this.config.get('backup.prefix'),
    }

    // バックアップ設定がなければバックアップをせずに終了
    if (!backup.bucket && !backup.prefix) {
      logger.debug(() => `バックアップ設定がないためバックアップを省略`)
      return
    }

    const source = {
      bucket: this.context.s3Bucket(),
      key: this.context.s3Key(),
    }

    const location = {
      bucket: this.constructor.scripting(backup.bucket || source.bucket, source),
      key: Path.join(this.constructor.scripting(backup.prefix, source), source.key),
    }

    if (source.bucket == location.bucket && source.key == location.key) {
      throw new Base.Warning(`s3://${location.bucket}/${location.key} は軽量化対象のファイルと同一のためバックアップを中断`)
    }

    // ダウンロード工程からentryを取得
    const entry = this.context.get('s3.entry')

    // バックアップを保存
    logger.trace(() => `バックアップファイルのアップロードを開始: s3://${location.bucket}/${location.key}`)
    entry.on('beforePut', params => logger.debug(() => `S3.putObject(${logger.constructor.inspect(params)})`))
    await entry.put({
      bucket: location.bucket,
      key: location.key,
      metadata: {
        'lightfile-status': 'backup',
      },
      acl: this.config.get('backup.acl'),
    })
    logger.trace(() => `バックアップファイルのアップロードを完了: s3://${location.bucket}/${location.key}`)
  }

  async uploadFile(logger) {
    const converted = this.context.get('convert.destTmp')
    const body = await new Promise((ok, ng) => Fs.readFile(converted.name, (err, buffer) => err ? ng(err) : ok(buffer)))

    // ダウンロード工程からentryを取得
    const entry = this.context.get('s3.entry')

    // ユーザーによる追加メタデータ
    const userMeta = this.config.getAsObject('upload.metadata')

    // LightFileによるメタデータ
    const lightfileMeta = {
      'lightfile-status': 'done',
      'lightfile-original-size': `${this.context.get('record.beforeSize')}`,
      // 'lightfile-after': `${this.context.get('record.afterSize')}`,
    }

    const metadata = _.merge(userMeta, lightfileMeta)
    logger.debug(() => `メタデータを更新: ${JSON.stringify(metadata)}`)

    // 上書き保存
    logger.trace(() => `上書きアップロードを開始`)
    entry.on('beforePut', params => logger.debug(() => `S3.putObject(${logger.constructor.inspect(params)})`))
    await entry.put({
      body,
      metadata,
      acl: this.config.get('upload.acl'),
    })
    logger.trace(() => `上書きアップロードを完了`)
  }

  async notify(logger) {
    const topicArn = this.config.get('sns.topicArn'),
      defaultValues = this.config.get('sns.defaultValues', {})

    if (!topicArn) {
      logger.trace(() => `SNSの設定がないため通知を省略`)
      return
    }

    const values = _.defaultsDeep({
      s3Event: this.context.get('rawEvent'),
      lambdaContext: this.context.get('lambdaContext'),
      s3: {
        bucket: this.context.s3Bucket(),
        key: this.context.s3Key(),
      },
      lightfile: _.pick(this.context.getAsObject('record'), [
        'beforeSize', 'afterSize', 'scriptClass',
      ]),
    }, defaultValues)

    const message = JSON.stringify(values)

    logger.trace(() => `SNSトピック(${topicArn})にメッセージ送信を開始: ${message}`)
    await this.sns.send(new PublishCommand({
      TopicArn: topicArn,
      Message: message,
    }))
    logger.trace(() => `SNSトピック(${topicArn})にメッセージ送信を完了: ${message}`)
  }

  buildResponse() {
    if (this.context.isS3BatchRequest()) {
      const event = this.context.get('rawEvent') || {}
      const record = this.constructor.loggerClass.queryItems('INFO').pop()

      // tasksに対応する配列
      const results = [
        {
          taskId: event.tasks[0].taskId,
          resultCode: 'Succeeded',
          resultString: record ? record.format() : 'Succeeded',
        }
      ]

      // 全体の戻り値
      return {
        invocationSchemaVersion: event.invocationSchemaVersion || null,
        treatMissingKeysAs: 'PermanentFailure',
        invocationId: event.invocationId || null,
        'results': results
      }
    } else {
      const record = this.constructor.loggerClass.queryItems('INFO').pop()
      return {
        results: [
          {
            status: 'done',
            message: record ? record.format() : '',
          }
        ]
      }
    }
    return {}
  }

  static createForHandlerArgs(event, lambdaContext) {
    const config = new LambdaConfig()
    config.mappingEnvs(process.env)
    const context = new LambdaContext(event, lambdaContext)
    const app = new LambdaApp(config, context)
    return app
  }
}

module.exports = {
  LambdaConfig,
  LambdaContext,
  LambdaApp,
}