import path from 'path'
import { S3Client, PutObjectCommand, ListObjectsCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import fs from 'fs';
import fspromise from 'fs/promises';
import rimraf from 'rimraf';
import mimeType from 'node-mime-types';

export default class ObjectStoreService {
  // You need to set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables for this service to work

  constructor(config) {
    this.path = path;
    this.s3 = new S3Client(config);

    this.downloadsInProgress = 0;
    this.uploadsInProgress = 0;
  }

  async uploadFile(filepath, params) {
    while (this.uploadsInProgress >= 10) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    let parameters = Object.assign({}, params);
    if (!parameters.Bucket) {
      throw new Error('ObjectStoreService; s3 bucket not defined in params');
    }

    try {
      this.uploadsInProgress++;
      const readStream = fs.createReadStream(this.path.resolve(filepath));

      readStream.once('error', (err) => {
        console.error(`unable to upload file ${filepath}, ${err.message}`);
      });

      parameters.Body = readStream;
      parameters.ContentType = mimeType.getMIMEType(filepath);

      const uploadCommand = new PutObjectCommand(parameters);
      const response = await this.s3.send(uploadCommand);

      console.info(`${parameters.Key} uploaded in bucket ${parameters.Bucket}`);
    } catch (e) {
      console.log("error uploading file " + filepath + " with params " + JSON.stringify(parameters) + " ERROR: " + e.message);
    } finally {
      this.uploadsInProgress--;
    }

    return true;
  }

  async uploadDirectory(path, params, rootKey) {

    let parameters = Object.assign({}, params);
    if (!parameters.Bucket) {
      throw new Error('ObjectStoreService; s3 bucket not defined in params');
    }
    const root = rootKey && rootKey.constructor === String ? rootKey : '';

    try {
      let dirPath = this.path.resolve(path);
      const dirStats = await fspromise.stat(dirPath);
      if (!dirStats.isDirectory()) {
        throw new Error(`ObjectStoreService.uploadDirectory; ${dirPath} is not a directory`);
      }

      const filenames = await fspromise.readdir(dirPath);

      if (Array.isArray(filenames)) {
        let promises = [];
        for (let index = 0; index < filenames.length; index++) {
          let filename = filenames[index];
          let filepath = `${dirPath}/${filename}`;
          let fileStats = await fspromise.stat(filepath);

          if (fileStats.isFile()) {
            let fileparameters = Object.assign({}, parameters);
            fileparameters.Key = root + '/' + filename;
            promises.push(this.uploadFile(filepath, fileparameters));
          } else if (fileStats.isDirectory()) {
            promises.push(this.uploadDirectory(filepath, params, root + '/' + filename));
          }
        }

        await Promise.all(promises);
      }
      console.info(`directory ${dirPath} successfully uploaded`);
    } catch (e) {
      console.error(`ObjectStoreService.uploadDirectory; unable to upload directory ${path}, ${e.message}`);
    }

    return true;
  };

  async downloadDirectory(bucket, dir = "", destination = "") {

    if (!destination) {
      throw new Error('ObjectStoreService.downloadDirectory; no destination folder given');
    }
    if (!dir.endsWith('/')) {
      dir = dir + '/'
    }
    if (!destination.endsWith('/')) {
      destination = destination + '/'
    }
    let path = destination + dir
    path = path.split('/').join(this.path.sep);
    if (fs.existsSync(path)) {
      try {
        rimraf.sync(path);
      } catch (error) {
        // retry
        if (fs.existsSync(path)) {
          await rimraf.sync(path);
        }
      }
    }
    fs.mkdirSync(path);
    const params = {
      Bucket: bucket,
      Delimiter: '/',
      Prefix: dir
    };

    const listCommand = new ListObjectsCommand(params);
    const response = await this.s3.send(listCommand);

    let folderPromises = [];
    if (response && (response.CommonPrefixes && Array.isArray(response.CommonPrefixes) && response.CommonPrefixes.length > 0)) {
      response.CommonPrefixes.forEach(folder => {
        folderPromises.push(this.downloadDirectory(bucket, folder.Prefix, destination));
      });
    }
    await Promise.all(folderPromises);
    let filePromises = [];
    if (response && (response.Contents && Array.isArray(response.Contents) && response.Contents.length > 0)) {
      response.Contents.forEach(file => {
        filePromises.push(this.downloadFile(bucket, file.Key, destination));
      });
    }
    await Promise.all(filePromises);
    return true;
  }

  async downloadFile(bucket, key, destination) {
    while (this.downloadsInProgress >= 10) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    let params = { Bucket: bucket, Key: key };
    let path = destination + key;
    path = path.split('/').join(this.path.sep);
    let file = fs.createWriteStream(path);

    const getCommand = new GetObjectCommand(params);
    this.downloadsInProgress++;
    const response = await this.s3.send(getCommand);
    await response.Body.pipe(file);
    this.downloadsInProgress--;
  }
}
