import path from 'path'
import { S3Client, PutObjectCommand, ListObjectsCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
import fs from 'fs';
import fspromise from 'fs/promises';
import rimraf from 'rimraf';
import mimeType from 'node-mime-types';
import * as tunnel from 'tunnel';
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { parseUrl } from '@smithy/url-parser';

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.clientConfig = {...config};
    
    if (config.useProxy === true) {
      const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy || 
                       process.env.HTTP_PROXY || process.env.http_proxy;
      
      if (proxyUrl) {
        console.info(`ObjectStoreService: Configuring tunnel agent for proxy`);
        
        try {
          // Parse the proxy URL
          const parsed = parseUrl(proxyUrl);
          const useHttps = this.clientConfig.endpoint && this.clientConfig.endpoint.startsWith('https');
          
          // Create a tunnel agent
          const agent = useHttps ? 
            tunnel.httpsOverHttp({
              proxy: {
                host: parsed.hostname,
                port: parsed.port || 80,
                headers: parsed.username && parsed.password ? {
                  'Proxy-Authorization': `Basic ${Buffer.from(`${parsed.username}:${parsed.password}`).toString('base64')}`
                } : undefined
              }
            }) :
            tunnel.httpOverHttp({
              proxy: {
                host: parsed.hostname,
                port: parsed.port || 80,
                headers: parsed.username && parsed.password ? {
                  'Proxy-Authorization': `Basic ${Buffer.from(`${parsed.username}:${parsed.password}`).toString('base64')}`
                } : undefined
              }
            });
          
          // Configure the SDK to use our agent
          this.clientConfig.requestHandler = new NodeHttpHandler({
            httpsAgent: useHttps ? agent : undefined,
            httpAgent: !useHttps ? agent : undefined,
            connectionTimeout: 30000
          });
          
        } catch (error) {
          console.error(`Error configuring proxy tunnel: ${error.message}`);
        }
      }
    }
    
    // Create a standard S3 client initially
    this.createStandardS3Client();
    
    // If role assumption is enabled, initialize it but don't wait for it in the constructor
    if (config.assumeRole === true) {
      this.assumeRoleInitialized = false;
      this.assumeRoleInitializing = this.initializeWithRoleAssumption();
    } else {
      this.assumeRoleInitialized = true;
      this.assumeRoleInitializing = Promise.resolve();
    }
    
    this.downloadsInProgress = 0;
    this.uploadsInProgress = 0;
  }

  // Create standard S3 client with current config
  createStandardS3Client() {
    try {
      this.s3 = new S3Client(this.clientConfig);
    } catch (error) {
      console.error(`Error creating standard S3 client: ${error.message}`);
      throw error; // Re-throw as this is critical
    }
  }

  async initializeWithRoleAssumption() {
    try {
      // Get the role ARN from environment variable
      const roleArn = process.env.AWS_ASSUME_ROLE;
      
      if (!roleArn) {
        console.error('AWS_ASSUME_ROLE environment variable is not set, but assumeRole is enabled');
        // Already using standard credentials from constructor
        this.assumeRoleInitialized = true;
        return;
      }

      console.info(`ObjectStoreService: Assuming role: ${roleArn}`);
      
      try {
        // Check if we have existing AWS credentials - they're required for assuming a role
        if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
          console.error('Cannot assume role without AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables');
          this.assumeRoleInitialized = true;
          return;
        }
        
        // Create a specific STS config that maintains region but removes any custom endpoint
        // to ensure we're using the standard AWS STS service
        const stsConfig = {
          region: this.clientConfig.region // Use the region from the client config
        };
        
        // If we have proxy settings, maintain those
        if (this.clientConfig.requestHandler) {
          stsConfig.requestHandler = this.clientConfig.requestHandler;
        }
        
        // Create an STS client
        const stsClient = new STSClient(stsConfig);
        
        // Create parameters for AssumeRole operation
        const params = {
          RoleArn: roleArn,
          RoleSessionName: `qa-s3-session-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
          DurationSeconds: 1800 // 30 minutes
        };
        
        console.log('Calling STS AssumeRole...');
        
        // Execute the assume role command
        const command = new AssumeRoleCommand(params);
        const response = await stsClient.send(command);
        
        console.log('STS AssumeRole response received');
        
        // Extract temporary credentials
        const credentials = response.Credentials;
        
        if (!credentials) {
          console.error('No credentials returned from AssumeRole operation');
          this.assumeRoleInitialized = true;
          return;
        }
        
        console.log('Credentials obtained from AssumeRole');
        
        // Create a new S3 client config with the temporary credentials
        const s3ConfigWithTemporaryCredentials = {
          ...this.clientConfig,
          credentials: {
            accessKeyId: credentials.AccessKeyId,
            secretAccessKey: credentials.SecretAccessKey,
            sessionToken: credentials.SessionToken
          }
        };
        
        // Create S3 client with temporary credentials
        this.s3 = new S3Client(s3ConfigWithTemporaryCredentials);
        
        console.info('Successfully assumed role and configured S3 client with temporary credentials');
        
      } catch (stsError) {
        // Handle STS client errors specifically
        console.error(`STS AssumeRole operation failed: ${stsError.name}: ${stsError.message}`);
        if (stsError.$metadata) {
          console.error(`STS response metadata: ${JSON.stringify(stsError.$metadata)}`);
        }
        if (stsError.stack) {
          console.error(`Stack trace: ${stsError.stack.split('\n')[0]}`);
        }
        
        // Ensure we have a valid S3 client (fallback to standard)
        this.createStandardS3Client();
      }
    } catch (error) {
      // Handle any other errors in the outer try block
      console.error(`Error assuming role: ${error.name}: ${error.message}`);
      if (error.stack) {
        console.error(`Stack trace: ${error.stack.split('\n')[0]}`);
      }
      
      // Ensure we have a valid S3 client
      this.createStandardS3Client();
    } finally {
      // Always mark initialization as complete
      this.assumeRoleInitialized = true;
    }
  }

  // Helper method to ensure role is initialized before performing operations
  async ensureRoleInitialized() {
    if (!this.assumeRoleInitialized) {
      try {
        await this.assumeRoleInitializing;
      } catch (error) {
        console.error(`Error waiting for role initialization: ${error.message}`);
        // Ensure we're marked as initialized even if there was an error
        this.assumeRoleInitialized = true;
      }
    }
  }

  async uploadFile(filepath, params) {
    // Ensure role assumption is complete before proceeding
    await this.ensureRoleInitialized();
    
    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) {
      // Avoid stringifying the entire error object which may contain circular references
      console.log(`error uploading file ${filepath} to bucket ${parameters.Bucket}, key ${parameters.Key}: ${e.name}: ${e.message}`);
    } finally {
      this.uploadsInProgress--;
    }

    return true;
  }

  async uploadDirectory(path, params, rootKey) {
    // Ensure role assumption is complete before proceeding
    await this.ensureRoleInitialized();

    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) {
      // Avoid JSON stringifying the error which may contain circular references
      console.error(`ObjectStoreService.uploadDirectory; unable to upload directory ${path}: ${e.name}: ${e.message}`);
    }

    return true;
  };

  async downloadDirectory(bucket, dir = "", destination = "") {
    // Ensure role assumption is complete before proceeding
    await this.ensureRoleInitialized();

    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) {
    // Ensure role assumption is complete before proceeding
    await this.ensureRoleInitialized();
    
    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--;
  }
}
