'use strict';

import { S3Client } from 'bun';
import * as C from './consts.js';
import type * as IT from './types.js';
import * as U from './utils.js';

/**
 * S3 class for interacting with S3-compatible object storage services.
 * This class provides methods for common S3 operations such as uploading, downloading,
 * and deleting objects, as well as multipart uploads.
 *
 * @class
 * @example
 * const s3 = new CoreS3({
 *   accessKeyId: 'your-access-key',
 *   secretAccessKey: 'your-secret-key',
 *   endpoint: 'https://your-s3-endpoint.com',
 *   region: 'us-east-1' // by default is auto
 * });
 *
 * // Upload a file
 * await s3.putObject('example.txt', 'Hello, World!');
 *
 * // Download a file
 * const content = await s3.getObject('example.txt');
 *
 * // Delete a file
 * await s3.deleteObject('example.txt');
 */
class S3mini {
  /**
   * Creates an instance of the S3 class.
   *
   * @constructor
   * @param {Object} config - Configuration options for the S3 instance.
   * @param {string} config.accessKeyId - The access key ID for authentication.
   * @param {string} config.secretAccessKey - The secret access key for authentication.
   * @param {string} config.endpoint - The endpoint URL of the S3-compatible service.
   * @param {string} [config.region='auto'] - The region of the S3 service.
   * @param {number} [config.requestSizeInBytes=8388608] - The request size of a single request in bytes (AWS S3 is 8MB).
   * @param {number} [config.requestAbortTimeout=undefined] - The timeout in milliseconds after which a request should be aborted (careful on streamed requests).
   * @param {Object} [config.logger=null] - A logger object with methods like info, warn, error.
   * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
   */
  private accessKeyId: string;
  private secretAccessKey: string;
  private endpoint: string;
  private region: string;
  private requestSizeInBytes: number;
  private requestAbortTimeout?: number;
  private logger?: IT.Logger;
  private signingKeyDate?: string;
  private signingKey?: Buffer;

  constructor({
    accessKeyId,
    secretAccessKey,
    endpoint,
    region = 'auto',
    requestSizeInBytes = C.DEFAULT_REQUEST_SIZE_IN_BYTES,
    requestAbortTimeout = undefined,
    logger = undefined,
  }: IT.S3Config) {
    this._validateConstructorParams(accessKeyId, secretAccessKey, endpoint);
    this.accessKeyId = accessKeyId;
    this.secretAccessKey = secretAccessKey;
    this.endpoint = this._ensureValidUrl(endpoint);
    this.region = region;
    this.requestSizeInBytes = requestSizeInBytes;
    this.requestAbortTimeout = requestAbortTimeout;
    this.logger = logger;
  }

  private _sanitize(obj: unknown): unknown {
    if (typeof obj !== 'object' || obj === null) {
      return obj;
    }
    return Object.keys(obj).reduce(
      (acc: Record<string, unknown>, key) => {
        if (C.SENSITIVE_KEYS_REDACTED.includes(key.toLowerCase())) {
          acc[key] = '[REDACTED]';
        } else if (
          typeof (obj as Record<string, unknown>)[key] === 'object' &&
          (obj as Record<string, unknown>)[key] !== null
        ) {
          acc[key] = this._sanitize((obj as Record<string, unknown>)[key]);
        } else {
          acc[key] = (obj as Record<string, unknown>)[key];
        }
        return acc;
      },
      Array.isArray(obj) ? [] : {},
    );
  }

  private _log(
    level: 'info' | 'warn' | 'error',
    message: string,
    additionalData: Record<string, unknown> | string = {},
  ): void {
    if (this.logger && typeof this.logger[level] === 'function') {
      // Function to recursively sanitize an object

      // Sanitize the additional data
      const sanitizedData = this._sanitize(additionalData);
      // Prepare the log entry
      const logEntry = {
        timestamp: new Date().toISOString(),
        level,
        message,
        details: sanitizedData,
        // Include some general context, but sanitize sensitive parts
        context: this._sanitize({
          region: this.region,
          endpoint: this.endpoint,
          // Only include the first few characters of the access key, if it exists
          accessKeyId: this.accessKeyId ? `${this.accessKeyId.substring(0, 4)}...` : undefined,
        }),
      };

      // Log the sanitized entry
      this.logger[level](JSON.stringify(logEntry));
    }
  }

  private _validateConstructorParams(accessKeyId: string, secretAccessKey: string, endpoint: string): void {
    if (typeof accessKeyId !== 'string' || accessKeyId.trim().length === 0) {
      throw new TypeError(C.ERROR_ACCESS_KEY_REQUIRED);
    }
    if (typeof secretAccessKey !== 'string' || secretAccessKey.trim().length === 0) {
      throw new TypeError(C.ERROR_SECRET_KEY_REQUIRED);
    }
    if (typeof endpoint !== 'string' || endpoint.trim().length === 0) {
      throw new TypeError(C.ERROR_ENDPOINT_REQUIRED);
    }
  }

  private _ensureValidUrl(raw: string): string {
    const candidate = /^(https?:)?\/\//i.test(raw) ? raw : `https://${raw}`;
    try {
      new URL(candidate);

      // Find the last non-slash character
      let endIndex = candidate.length;
      while (endIndex > 0 && candidate[endIndex - 1] === '/') {
        endIndex--;
      }
      return endIndex === candidate.length ? candidate : candidate.substring(0, endIndex);
    } catch {
      const msg = `${C.ERROR_ENDPOINT_FORMAT} But provided: "${raw}"`;
      this._log('error', msg);
      throw new TypeError(msg);
    }
  }

  private _validateMethodIsGetOrHead(method: string): void {
    if (method !== 'GET' && method !== 'HEAD') {
      this._log('error', `${C.ERROR_PREFIX}method must be either GET or HEAD`);
      throw new Error(`${C.ERROR_PREFIX}method must be either GET or HEAD`);
    }
  }

  private _checkKey(key: string): void {
    if (typeof key !== 'string' || key.trim().length === 0) {
      this._log('error', C.ERROR_KEY_REQUIRED);
      throw new TypeError(C.ERROR_KEY_REQUIRED);
    }
  }

  private _checkDelimiter(delimiter: string): void {
    if (typeof delimiter !== 'string' || delimiter.trim().length === 0) {
      this._log('error', C.ERROR_DELIMITER_REQUIRED);
      throw new TypeError(C.ERROR_DELIMITER_REQUIRED);
    }
  }

  private _checkPrefix(prefix: string): void {
    if (typeof prefix !== 'string') {
      this._log('error', C.ERROR_PREFIX_TYPE);
      throw new TypeError(C.ERROR_PREFIX_TYPE);
    }
  }

  // private _checkMaxKeys(maxKeys: number): void {
  //   if (typeof maxKeys !== 'number' || maxKeys <= 0) {
  //     this._log('error', C.ERROR_MAX_KEYS_TYPE);
  //     throw new TypeError(C.ERROR_MAX_KEYS_TYPE);
  //   }
  // }

  private _checkOpts(opts: object): void {
    if (typeof opts !== 'object') {
      this._log('error', `${C.ERROR_PREFIX}opts must be an object`);
      throw new TypeError(`${C.ERROR_PREFIX}opts must be an object`);
    }
  }

  private _filterIfHeaders(opts: Record<string, unknown>): {
    filteredOpts: Record<string, string>;
    conditionalHeaders: Record<string, unknown>;
  } {
    const filteredOpts: Record<string, string> = {};
    const conditionalHeaders: Record<string, unknown> = {};
    const ifHeaders = ['if-match', 'if-none-match', 'if-modified-since', 'if-unmodified-since'];

    for (const [key, value] of Object.entries(opts)) {
      if (ifHeaders.includes(key.toLowerCase())) {
        // Convert to lowercase for consistency
        conditionalHeaders[key] = value;
      } else {
        filteredOpts[key] = value as string;
      }
    }

    return { filteredOpts, conditionalHeaders };
  }

  private _validateUploadPartParams(
    key: string,
    uploadId: string,
    data: Buffer | string,
    partNumber: number,
    opts: object,
  ): void {
    this._checkKey(key);
    if (!(data instanceof Buffer || typeof data === 'string')) {
      this._log('error', C.ERROR_DATA_BUFFER_REQUIRED);
      throw new TypeError(C.ERROR_DATA_BUFFER_REQUIRED);
    }
    if (typeof uploadId !== 'string' || uploadId.trim().length === 0) {
      this._log('error', C.ERROR_UPLOAD_ID_REQUIRED);
      throw new TypeError(C.ERROR_UPLOAD_ID_REQUIRED);
    }
    if (!Number.isInteger(partNumber) || partNumber <= 0) {
      this._log('error', `${C.ERROR_PREFIX}partNumber must be a positive integer`);
      throw new TypeError(`${C.ERROR_PREFIX}partNumber must be a positive integer`);
    }
    this._checkOpts(opts);
  }

  private _sign(
    method: IT.HttpMethod,
    keyPath: string,
    query: Record<string, unknown> = {},
    headers: Record<string, string | number> = {},
  ): { url: string; headers: Record<string, string | number> } {
    // Create URL without appending keyPath first
    const url = new URL(this.endpoint);

    // Properly format the pathname to avoid double slashes
    if (keyPath && keyPath.length > 0) {
      url.pathname =
        url.pathname === '/' ? `/${keyPath.replace(/^\/+/, '')}` : `${url.pathname}/${keyPath.replace(/^\/+/, '')}`;
    }

    const fullDatetime = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
    const shortDatetime = fullDatetime.slice(0, 8);
    const credentialScope = this._buildCredentialScope(shortDatetime);

    headers[C.HEADER_AMZ_CONTENT_SHA256] = C.UNSIGNED_PAYLOAD; // body ? U.hash(body) : C.UNSIGNED_PAYLOAD;
    headers[C.HEADER_AMZ_DATE] = fullDatetime;
    headers[C.HEADER_HOST] = url.host;
    // sort headers alphabetically by key
    const ignoredHeaders = ['authorization', 'content-length', 'content-type', 'user-agent'];
    let headersForSigning = Object.fromEntries(
      Object.entries(headers).filter(([key]) => !ignoredHeaders.includes(key.toLowerCase())),
    );

    headersForSigning = Object.fromEntries(
      Object.entries(headersForSigning).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)),
    );
    const canonicalHeaders = this._buildCanonicalHeaders(headersForSigning);
    const signedHeaders = Object.keys(headersForSigning)
      .map(key => key.toLowerCase())
      .sort()
      .join(';');

    const canonicalRequest = this._buildCanonicalRequest(method, url, query, canonicalHeaders, signedHeaders);
    const stringToSign = this._buildStringToSign(fullDatetime, credentialScope, canonicalRequest);
    const signature = this._calculateSignature(shortDatetime, stringToSign);
    const authorizationHeader = this._buildAuthorizationHeader(credentialScope, signedHeaders, signature);
    headers[C.HEADER_AUTHORIZATION] = authorizationHeader;
    return { url: url.toString(), headers };
  }

  private _buildCanonicalHeaders(headers: Record<string, string | number>): string {
    return Object.entries(headers)
      .map(([key, value]) => `${key.toLowerCase()}:${String(value).trim()}`)
      .join('\n');
  }

  private _buildCanonicalRequest(
    method: IT.HttpMethod,
    url: URL,
    query: Record<string, unknown>,
    canonicalHeaders: string,
    signedHeaders: string,
  ): string {
    const parts = [
      method,
      url.pathname,
      this._buildCanonicalQueryString(query),
      canonicalHeaders + '\n', // Canonical headers end with extra newline
      signedHeaders,
      C.UNSIGNED_PAYLOAD,
    ];
    return parts.join('\n');
  }

  private _buildCredentialScope(shortDatetime: string): string {
    return [shortDatetime, this.region, C.S3_SERVICE, C.AWS_REQUEST_TYPE].join('/');
  }

  private _buildStringToSign(fullDatetime: string, credentialScope: string, canonicalRequest: string): string {
    return [C.AWS_ALGORITHM, fullDatetime, credentialScope, U.hash(canonicalRequest)].join('\n');
  }

  private _calculateSignature(shortDatetime: string, stringToSign: string): string {
    if (shortDatetime !== this.signingKeyDate) {
      this.signingKeyDate = shortDatetime;
      this.signingKey = this._getSignatureKey(shortDatetime);
    }
    return U.hmac(this.signingKey!, stringToSign, 'hex') as string;
  }

  private _buildAuthorizationHeader(credentialScope: string, signedHeaders: string, signature: string): string {
    return [
      `${C.AWS_ALGORITHM} Credential=${this.accessKeyId}/${credentialScope}`,
      `SignedHeaders=${signedHeaders}`,
      `Signature=${signature}`,
    ].join(', ');
  }

  private async _signedRequest(
    method: IT.HttpMethod, // 'GET' | 'HEAD' | 'PUT' | 'POST' | 'DELETE'
    key: string, // ‘’ allowed for bucket‑level ops
    {
      query = {}, // ?query=string
      body = '', // string | Buffer | undefined
      headers = {}, // extra/override headers
      tolerated = [], // [200, 404] etc.
      withQuery = false, // append query string to signed URL
    }: {
      query?: Record<string, unknown> | undefined;
      body?: string | Buffer | undefined;
      headers?: Record<string, string | number | undefined> | IT.SSECHeaders | undefined;
      tolerated?: number[] | undefined;
      withQuery?: boolean | undefined;
    } = {},
  ): Promise<Response> {
    // Basic validation
    if (!['GET', 'HEAD', 'PUT', 'POST', 'DELETE'].includes(method)) {
      throw new Error(`${C.ERROR_PREFIX}Unsupported HTTP method ${method as string}`);
    }

    const { filteredOpts, conditionalHeaders } = ['GET', 'HEAD'].includes(method)
      ? this._filterIfHeaders(query)
      : { filteredOpts: query, conditionalHeaders: {} };
    const baseHeaders: Record<string, string | number> = {
      [C.HEADER_AMZ_CONTENT_SHA256]: C.UNSIGNED_PAYLOAD,
      // ...(['GET', 'HEAD'].includes(method) ? { [C.HEADER_CONTENT_TYPE]: C.JSON_CONTENT_TYPE } : {}),
      ...headers,
      ...conditionalHeaders,
    };

    const encodedKey = key ? U.uriResourceEscape(key) : '';
    const { url, headers: signedHeaders } = this._sign(method, encodedKey, filteredOpts, baseHeaders);
    if (Object.keys(query).length > 0) {
      withQuery = true; // append query string to signed URL
    }
    const filteredOptsStrings = Object.fromEntries(
      Object.entries(filteredOpts).map(([k, v]) => [k, String(v)]),
    ) as Record<string, string>;
    const finalUrl =
      withQuery && Object.keys(filteredOpts).length ? `${url}?${new URLSearchParams(filteredOptsStrings)}` : url;
    const signedHeadersString = Object.fromEntries(
      Object.entries(signedHeaders).map(([k, v]) => [k, String(v)]),
    ) as Record<string, string>;
    return this._sendRequest(finalUrl, method, signedHeadersString, body, tolerated);
  }

  /**
   * Gets the current configuration properties of the S3 instance.
   * @returns {IT.S3Config} The current S3 configuration object containing all settings.
   * @example
   * const config = s3.getProps();
   * console.log(config.endpoint); // 'https://s3.amazonaws.com/my-bucket'
   */
  public getProps(): IT.S3Config {
    return {
      accessKeyId: this.accessKeyId,
      secretAccessKey: this.secretAccessKey,
      endpoint: this.endpoint,
      region: this.region,
      requestSizeInBytes: this.requestSizeInBytes,
      requestAbortTimeout: this.requestAbortTimeout,
      logger: this.logger,
    };
  }

  /**
   * Updates the configuration properties of the S3 instance.
   * @param {IT.S3Config} props - The new configuration object.
   * @param {string} props.accessKeyId - The access key ID for authentication.
   * @param {string} props.secretAccessKey - The secret access key for authentication.
   * @param {string} props.endpoint - The endpoint URL of the S3-compatible service.
   * @param {string} [props.region='auto'] - The region of the S3 service.
   * @param {number} [props.requestSizeInBytes=8388608] - The request size of a single request in bytes.
   * @param {number} [props.requestAbortTimeout] - The timeout in milliseconds after which a request should be aborted.
   * @param {IT.Logger} [props.logger] - A logger object with methods like info, warn, error.
   * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
   * @example
   * s3.setProps({
   *   accessKeyId: 'new-access-key',
   *   secretAccessKey: 'new-secret-key',
   *   endpoint: 'https://new-endpoint.com/my-bucket',
   *   region: 'us-west-2' // by default is auto
   * });
   */
  public setProps(props: IT.S3Config): void {
    this._validateConstructorParams(props.accessKeyId, props.secretAccessKey, props.endpoint);
    this.accessKeyId = props.accessKeyId;
    this.secretAccessKey = props.secretAccessKey;
    this.region = props.region || 'auto';
    this.endpoint = props.endpoint;
    this.requestSizeInBytes = props.requestSizeInBytes || C.DEFAULT_REQUEST_SIZE_IN_BYTES;
    this.requestAbortTimeout = props.requestAbortTimeout;
    this.logger = props.logger;
  }

  /**
   * Sanitizes an ETag value by removing surrounding quotes and whitespace.
   * Still returns RFC compliant ETag. https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3
   * @param {string} etag - The ETag value to sanitize.
   * @returns {string} The sanitized ETag value.
   * @example
   * const cleanEtag = s3.sanitizeETag('"abc123"'); // Returns: 'abc123'
   */
  public sanitizeETag(etag: string): string {
    return U.sanitizeETag(etag);
  }

  /**
   * Creates a new bucket.
   * This method sends a request to create a new bucket in the specified in endpoint.
   * @returns A promise that resolves to true if the bucket was created successfully, false otherwise.
   */
  public async createBucket(): Promise<boolean> {
    const xmlBody = `
      <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
        <LocationConstraint>${this.region}</LocationConstraint>
      </CreateBucketConfiguration>
    `;
    const headers = {
      [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
      [C.HEADER_CONTENT_LENGTH]: Buffer.byteLength(xmlBody).toString(),
    };
    const res = await this._signedRequest('PUT', '', {
      body: xmlBody,
      headers,
      tolerated: [200, 404, 403, 409], // don’t throw on 404/403 // 409 = bucket already exists
    });
    return res.status === 200;
  }

  /**
   * Checks if a bucket exists.
   * This method sends a request to check if the specified bucket exists in the S3-compatible service.
   * @returns A promise that resolves to true if the bucket exists, false otherwise.
   */
  public async bucketExists(): Promise<boolean> {
    const res = await this._signedRequest('HEAD', '', { tolerated: [200, 404, 403] });
    return res.status === 200;
  }

  /**
   * Lists objects in the bucket with optional filtering and no pagination.
   * This method retrieves all objects matching the criteria (not paginated like listObjectsV2).
   * @param {string} [delimiter='/'] - The delimiter to use for grouping objects.
   * @param {string} [prefix=''] - The prefix to filter objects by.
   * @param {number} [maxKeys] - The maximum number of keys to return. If not provided, all keys will be returned.
   * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
   * @returns {Promise<IT.ListObject[] | null>} A promise that resolves to an array of objects or null if the bucket is empty.
   * @example
   * // List all objects
   * const objects = await s3.listObjects();
   *
   * // List objects with prefix
   * const photos = await s3.listObjects('/', 'photos/', 100);
   */
  public async listObjects(
    delimiter: string = '/',
    prefix: string = '',
    maxKeys?: number,
    // method: IT.HttpMethod = 'GET', // 'GET' or 'HEAD'
    opts: Record<string, unknown> = {},
  ): Promise<IT.ListObject[] | null | Awaited<ReturnType<S3Client['list']>>["contents"]> {
    this._checkDelimiter(delimiter);
    this._checkPrefix(prefix);
    this._checkOpts(opts);

    const keyPath = delimiter === '/' ? delimiter : U.uriEscape(delimiter);

    const unlimited = !(maxKeys && maxKeys > 0);
    let remaining = unlimited ? Infinity : maxKeys;
    let token: string | undefined;
    const all: IT.ListObject[] = [];

    do {
      const batchSize = Math.min(remaining, 1000); // S3 ceiling
      const query: Record<string, unknown> = {
        'list-type': C.LIST_TYPE, // =2 for V2
        'max-keys': String(batchSize),
        ...(prefix ? { prefix } : {}),
        ...(token ? { 'continuation-token': token } : {}),
        ...opts,
      };

      const res = await this._signedRequest('GET', keyPath, {
        query,
        withQuery: true,
        tolerated: [200, 404],
      });

      if (res.status === 404) {
        return null;
      }
      if (res.status !== 200) {
        const errorBody = await res.text();
        const errorCode = res.headers.get('x-amz-error-code') || 'Unknown';
        const errorMessage = res.headers.get('x-amz-error-message') || res.statusText;
        this._log(
          'error',
          `${C.ERROR_PREFIX}Request failed with status ${res.status}: ${errorCode} - ${errorMessage}, err body: ${errorBody}`,
        );
        throw new Error(
          `${C.ERROR_PREFIX}Request failed with status ${res.status}: ${errorCode} - ${errorMessage}, err body: ${errorBody}`,
        );
      }
      const raw = U.parseXml(await res.text()) as Record<string, unknown>;
      if (typeof raw !== 'object' || !raw || 'error' in raw) {
        this._log('error', `${C.ERROR_PREFIX}Unexpected listObjects response shape: ${JSON.stringify(raw)}`);
        throw new Error(`${C.ERROR_PREFIX}Unexpected listObjects response shape`);
      }
      const out = (raw.ListBucketResult || raw.listBucketResult || raw) as Record<string, unknown>;
      /* accumulate Contents */
      const contents = out.Contents || out.contents; // S3 v2 vs v1
      if (contents) {
        const batch = Array.isArray(contents) ? contents : [contents];
        all.push(...(batch as IT.ListObject[]));
        if (!unlimited) {
          remaining -= batch.length;
        }
      }
      const truncated = out.IsTruncated === 'true' || out.isTruncated === 'true' || false;
      token = truncated
        ? ((out.NextContinuationToken || out.nextContinuationToken || out.NextMarker || out.nextMarker) as
            | string
            | undefined)
        : undefined;
    } while (token && remaining > 0);

    return all;
  }

  /**
   * Lists multipart uploads in the bucket.
   * This method sends a request to list multipart uploads in the specified bucket.
   * @param {string} [delimiter='/'] - The delimiter to use for grouping uploads.
   * @param {string} [prefix=''] - The prefix to filter uploads by.
   * @param {IT.HttpMethod} [method='GET'] - The HTTP method to use for the request (GET or HEAD).
   * @param {Record<string, string | number | boolean | undefined>} [opts={}] - Additional options for the request.
   * @returns A promise that resolves to a list of multipart uploads or an error.
   */
  public async listMultipartUploads(
    delimiter: string = '/',
    prefix: string = '',
    method: IT.HttpMethod = 'GET',
    opts: Record<string, string | number | boolean | undefined> = {},
  ): Promise<IT.ListMultipartUploadSuccess | IT.MultipartUploadError> {
    this._checkDelimiter(delimiter);
    this._checkPrefix(prefix);
    this._validateMethodIsGetOrHead(method);
    this._checkOpts(opts);

    const query = { uploads: '', ...opts };
    const keyPath = delimiter === '/' ? delimiter : U.uriEscape(delimiter);

    const res = await this._signedRequest(method, keyPath, {
      query,
      withQuery: true,
    });
    // doublecheck if this is needed
    // if (method === 'HEAD') {
    //   return {
    //     size: +(res.headers.get(C.HEADER_CONTENT_LENGTH) ?? '0'),
    //     mtime: res.headers.get(C.HEADER_LAST_MODIFIED) ? new Date(res.headers.get(C.HEADER_LAST_MODIFIED)!) : undefined,
    //     etag: res.headers.get(C.HEADER_ETAG) ?? '',
    //   };
    // }
    const raw = U.parseXml(await res.text()) as unknown;
    if (typeof raw !== 'object' || raw === null) {
      throw new Error(`${C.ERROR_PREFIX}Unexpected listMultipartUploads response shape`);
    }
    if ('listMultipartUploadsResult' in raw) {
      return raw.listMultipartUploadsResult as IT.ListMultipartUploadSuccess;
    }
    return raw as IT.MultipartUploadError;
  }

  /**
   * Get an object from the S3-compatible service.
   * This method sends a request to retrieve the specified object from the S3-compatible service.
   * @param {string} key - The key of the object to retrieve.
   * @param {Record<string, unknown>} [opts] - Additional options for the request.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns A promise that resolves to the object data (string) or null if not found.
   */
  public async getObject(
    key: string,
    opts: Record<string, unknown> = {},
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<string | null> {
    // if ssecHeaders is set, add it to headers
    const res = await this._signedRequest('GET', key, {
      query: opts, // use opts.query if it exists, otherwise use an empty object
      tolerated: [200, 404, 412, 304],
      headers: ssecHeaders ? { ...ssecHeaders } : undefined,
    });
    if ([404, 412, 304].includes(res.status)) {
      return null;
    }
    return res.text();
  }

  /**
   * Get an object response from the S3-compatible service.
   * This method sends a request to retrieve the specified object and returns the full response.
   * @param {string} key - The key of the object to retrieve.
   * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns A promise that resolves to the Response object or null if not found.
   */
  public async getObjectResponse(
    key: string,
    opts: Record<string, unknown> = {},
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<Response | null> {
    const res = await this._signedRequest('GET', key, {
      query: opts,
      tolerated: [200, 404, 412, 304],
      headers: ssecHeaders ? { ...ssecHeaders } : undefined,
    });
    if ([404, 412, 304].includes(res.status)) {
      return null;
    }
    return res;
  }

  /**
   * Get an object as an ArrayBuffer from the S3-compatible service.
   * This method sends a request to retrieve the specified object and returns it as an ArrayBuffer.
   * @param {string} key - The key of the object to retrieve.
   * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns A promise that resolves to the object data as an ArrayBuffer or null if not found.
   */
  public async getObjectArrayBuffer(
    key: string,
    opts: Record<string, unknown> = {},
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<ArrayBuffer | null> {
    const res = await this._signedRequest('GET', key, {
      query: opts,
      tolerated: [200, 404, 412, 304],
      headers: ssecHeaders ? { ...ssecHeaders } : undefined,
    });
    if ([404, 412, 304].includes(res.status)) {
      return null;
    }
    return res.arrayBuffer();
  }

  /**
   * Get an object as JSON from the S3-compatible service.
   * This method sends a request to retrieve the specified object and returns it as JSON.
   * @param {string} key - The key of the object to retrieve.
   * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns A promise that resolves to the object data as JSON or null if not found.
   */
  public async getObjectJSON<T = unknown>(
    key: string,
    opts: Record<string, unknown> = {},
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<T | null> {
    const res = await this._signedRequest('GET', key, {
      query: opts,
      tolerated: [200, 404, 412, 304],
      headers: ssecHeaders ? { ...ssecHeaders } : undefined,
    });
    if ([404, 412, 304].includes(res.status)) {
      return null;
    }
    return res.json() as Promise<T>;
  }

  /**
   * Get an object with its ETag from the S3-compatible service.
   * This method sends a request to retrieve the specified object and its ETag.
   * @param {string} key - The key of the object to retrieve.
   * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns A promise that resolves to an object containing the ETag and the object data as an ArrayBuffer or null if not found.
   */
  public async getObjectWithETag(
    key: string,
    opts: Record<string, unknown> = {},
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<{ etag: string | null; data: ArrayBuffer | null }> {
    try {
      const res = await this._signedRequest('GET', key, {
        query: opts,
        tolerated: [200, 404, 412, 304],
        headers: ssecHeaders ? { ...ssecHeaders } : undefined,
      });

      if ([404, 412, 304].includes(res.status)) {
        return { etag: null, data: null };
      }

      const etag = res.headers.get(C.HEADER_ETAG);
      if (!etag) {
        throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
      }
      return { etag: U.sanitizeETag(etag), data: await res.arrayBuffer() };
    } catch (err) {
      this._log('error', `Error getting object ${key} with ETag: ${String(err)}`);
      throw err;
    }
  }

  /**
   * Get an object as a raw response from the S3-compatible service.
   * This method sends a request to retrieve the specified object and returns the raw response.
   * @param {string} key - The key of the object to retrieve.
   * @param {boolean} [wholeFile=true] - Whether to retrieve the whole file or a range.
   * @param {number} [rangeFrom=0] - The starting byte for the range (if not whole file).
   * @param {number} [rangeTo=this.requestSizeInBytes] - The ending byte for the range (if not whole file).
   * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns A promise that resolves to the Response object.
   */
  public async getObjectRaw(
    key: string,
    wholeFile = true,
    rangeFrom = 0,
    rangeTo = this.requestSizeInBytes,
    opts: Record<string, unknown> = {},
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<Response> {
    const rangeHdr: Record<string, string | number> = wholeFile ? {} : { range: `bytes=${rangeFrom}-${rangeTo - 1}` };

    return this._signedRequest('GET', key, {
      query: { ...opts },
      headers: { ...rangeHdr, ...ssecHeaders },
      withQuery: true, // keep ?query=string behaviour
    });
  }

  /**
   * Get the content length of an object.
   * This method sends a HEAD request to retrieve the content length of the specified object.
   * @param {string} key - The key of the object to retrieve the content length for.
   * @returns A promise that resolves to the content length of the object in bytes, or 0 if not found.
   * @throws {Error} If the content length header is not found in the response.
   */
  public async getContentLength(key: string, ssecHeaders?: IT.SSECHeaders): Promise<number> {
    try {
      const res = await this._signedRequest('HEAD', key, {
        headers: ssecHeaders ? { ...ssecHeaders } : undefined,
      });
      const len = res.headers.get(C.HEADER_CONTENT_LENGTH);
      return len ? +len : 0;
    } catch (err) {
      this._log('error', `Error getting content length for object ${key}: ${String(err)}`);
      throw new Error(`${C.ERROR_PREFIX}Error getting content length for object ${key}: ${String(err)}`);
    }
  }

  /**
   * Checks if an object exists in the S3-compatible service.
   * This method sends a HEAD request to check if the specified object exists.
   * @param {string} key - The key of the object to check.
   * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
   * @returns A promise that resolves to true if the object exists, false if not found, or null if ETag mismatch.
   */
  public async objectExists(key: string, opts: Record<string, unknown> = {}): Promise<IT.ExistResponseCode> {
    const res = await this._signedRequest('HEAD', key, {
      query: opts,
      tolerated: [200, 404, 412, 304],
    });

    if (res.status === 404) {
      return false; // not found
    }
    if (res.status === 412 || res.status === 304) {
      return null; // ETag mismatch
    }
    return true; // found (200)
  }

  /**
   * Retrieves the ETag of an object without downloading its content.
   * @param {string} key - The key of the object to retrieve the ETag for.
   * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns {Promise<string | null>} A promise that resolves to the ETag value or null if the object is not found.
   * @throws {Error} If the ETag header is not found in the response.
   * @example
   * const etag = await s3.getEtag('path/to/file.txt');
   * if (etag) {
   *   console.log(`File ETag: ${etag}`);
   * }
   */
  public async getEtag(
    key: string,
    opts: Record<string, unknown> = {},
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<string | null> {
    const res = await this._signedRequest('HEAD', key, {
      query: opts,
      tolerated: [200, 304, 404, 412],
      headers: ssecHeaders ? { ...ssecHeaders } : undefined,
    });

    if (res.status === 404) {
      return null;
    }

    if (res.status === 412 || res.status === 304) {
      return null; // ETag mismatch
    }

    const etag = res.headers.get(C.HEADER_ETAG);
    if (!etag) {
      throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
    }

    return U.sanitizeETag(etag);
  }

  /**
   * Uploads an object to the S3-compatible service.
   * @param {string} key - The key/path where the object will be stored.
   * @param {string | Buffer} data - The data to upload (string or Buffer).
   * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns {Promise<Response | number>} A promise that resolves to the Response object from the upload request.
   * @throws {TypeError} If data is not a string or Buffer.
   * @example
   * // Upload text file
   * await s3.putObject('hello.txt', 'Hello, World!', 'text/plain');
   *
   * // Upload binary data
   * const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
   * await s3.putObject('image.png', buffer, 'image/png');
   */
  public async putObject(
    key: string,
    data: string | Buffer,
    fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<Response | number> {
    if (!(data instanceof Buffer || typeof data === 'string')) {
      throw new TypeError(C.ERROR_DATA_BUFFER_REQUIRED);
    }
    return this._signedRequest('PUT', key, {
      body: data,
      headers: {
        [C.HEADER_CONTENT_LENGTH]: typeof data === 'string' ? Buffer.byteLength(data) : data.length,
        [C.HEADER_CONTENT_TYPE]: fileType,
        ...ssecHeaders,
      },
      tolerated: [200],
    });
  }

  /**
   * Initiates a multipart upload and returns the upload ID.
   * @param {string} key - The key/path where the object will be stored.
   * @param {string} [fileType='application/octet-stream'] - The MIME type of the file.
   * @param {IT.SSECHeaders?} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns {Promise<string>} A promise that resolves to the upload ID for the multipart upload.
   * @throws {TypeError} If key is invalid or fileType is not a string.
   * @throws {Error} If the multipart upload fails to initialize.
   * @example
   * const uploadId = await s3.getMultipartUploadId('large-file.zip', 'application/zip');
   * console.log(`Started multipart upload: ${uploadId}`);
   */
  public async getMultipartUploadId(
    key: string,
    fileType: string = C.DEFAULT_STREAM_CONTENT_TYPE,
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<string> {
    this._checkKey(key);
    if (typeof fileType !== 'string') {
      throw new TypeError(`${C.ERROR_PREFIX}fileType must be a string`);
    }
    const query = { uploads: '' };
    const headers = { [C.HEADER_CONTENT_TYPE]: fileType, ...ssecHeaders };

    const res = await this._signedRequest('POST', key, {
      query,
      headers,
      withQuery: true,
    });
    const parsed = U.parseXml(await res.text()) as Record<string, unknown>;

    // if (
    //   parsed &&
    //   typeof parsed === 'object' &&
    //   'initiateMultipartUploadResult' in parsed &&
    //   parsed.initiateMultipartUploadResult &&
    //   'uploadId' in (parsed.initiateMultipartUploadResult as { uploadId: string })
    // ) {
    //   return (parsed.initiateMultipartUploadResult as { uploadId: string }).uploadId;
    // }

    if (parsed && typeof parsed === 'object') {
      // Check for both cases of InitiateMultipartUploadResult
      const uploadResult =
        (parsed.initiateMultipartUploadResult as Record<string, unknown>) ||
        (parsed.InitiateMultipartUploadResult as Record<string, unknown>);

      if (uploadResult && typeof uploadResult === 'object') {
        // Check for both cases of uploadId
        const uploadId = uploadResult.uploadId || uploadResult.UploadId;

        if (uploadId && typeof uploadId === 'string') {
          return uploadId;
        }
      }
    }

    throw new Error(`${C.ERROR_PREFIX}Failed to create multipart upload: ${JSON.stringify(parsed)}`);
  }

  /**
   * Uploads a part in a multipart upload.
   * @param {string} key - The key of the object being uploaded.
   * @param {string} uploadId - The upload ID from getMultipartUploadId.
   * @param {Buffer | string} data - The data for this part.
   * @param {number} partNumber - The part number (must be between 1 and 10,000).
   * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns {Promise<IT.UploadPart>} A promise that resolves to an object containing the partNumber and etag.
   * @throws {TypeError} If any parameter is invalid.
   * @example
   * const part = await s3.uploadPart(
   *   'large-file.zip',
   *   uploadId,
   *   partData,
   *   1
   * );
   * console.log(`Part ${part.partNumber} uploaded with ETag: ${part.etag}`);
   */
  public async uploadPart(
    key: string,
    uploadId: string,
    data: Buffer | string,
    partNumber: number,
    opts: Record<string, unknown> = {},
    ssecHeaders?: IT.SSECHeaders,
  ): Promise<IT.UploadPart> {
    this._validateUploadPartParams(key, uploadId, data, partNumber, opts);

    const query = { uploadId, partNumber, ...opts };
    const res = await this._signedRequest('PUT', key, {
      query,
      body: data,
      headers: {
        [C.HEADER_CONTENT_LENGTH]: typeof data === 'string' ? Buffer.byteLength(data) : data.length,
        ...ssecHeaders,
      },
    });

    return { partNumber, etag: U.sanitizeETag(res.headers.get('etag') || '') };
  }

  /**
   * Completes a multipart upload by combining all uploaded parts.
   * @param {string} key - The key of the object being uploaded.
   * @param {string} uploadId - The upload ID from getMultipartUploadId.
   * @param {Array<IT.UploadPart>} parts - Array of uploaded parts with partNumber and etag.
   * @returns {Promise<IT.CompleteMultipartUploadResult>} A promise that resolves to the completion result containing the final ETag.
   * @throws {Error} If the multipart upload fails to complete.
   * @example
   * const result = await s3.completeMultipartUpload(
   *   'large-file.zip',
   *   uploadId,
   *   [
   *     { partNumber: 1, etag: 'abc123' },
   *     { partNumber: 2, etag: 'def456' }
   *   ]
   * );
   * console.log(`Upload completed with ETag: ${result.etag}`);
   */
  public async completeMultipartUpload(
    key: string,
    uploadId: string,
    parts: Array<IT.UploadPart>,
  ): Promise<IT.CompleteMultipartUploadResult> {
    const query = { uploadId };
    const xmlBody = this._buildCompleteMultipartUploadXml(parts);
    const headers = {
      [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
      [C.HEADER_CONTENT_LENGTH]: Buffer.byteLength(xmlBody).toString(),
    };

    const res = await this._signedRequest('POST', key, {
      query,
      body: xmlBody,
      headers,
      withQuery: true,
    });

    const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
    if (parsed && typeof parsed === 'object') {
      // Check for both cases
      const result = parsed.completeMultipartUploadResult || parsed.CompleteMultipartUploadResult || parsed;

      if (result && typeof result === 'object') {
        const resultObj = result as Record<string, unknown>;

        // Handle ETag in all its variations
        const etag = resultObj.ETag || resultObj.eTag || resultObj.etag;
        if (etag && typeof etag === 'string') {
          return {
            ...resultObj,
            etag: this.sanitizeETag(etag),
          } as IT.CompleteMultipartUploadResult;
        }

        return result as IT.CompleteMultipartUploadResult;
      }
    }

    throw new Error(`${C.ERROR_PREFIX}Failed to complete multipart upload: ${JSON.stringify(parsed)}`);
  }

  /**
   * Aborts a multipart upload and removes all uploaded parts.
   * @param {string} key - The key of the object being uploaded.
   * @param {string} uploadId - The upload ID to abort.
   * @param {IT.SSECHeaders} [ssecHeaders] - Server-Side Encryption headers, if any.
   * @returns {Promise<object>} A promise that resolves to an object containing the abort status and details.
   * @throws {TypeError} If key or uploadId is invalid.
   * @throws {Error} If the abort operation fails.
   * @example
   * try {
   *   const result = await s3.abortMultipartUpload('large-file.zip', uploadId);
   *   console.log('Upload aborted:', result.status);
   * } catch (error) {
   *   console.error('Failed to abort upload:', error);
   * }
   */
  public async abortMultipartUpload(key: string, uploadId: string, ssecHeaders?: IT.SSECHeaders): Promise<object> {
    this._checkKey(key);
    if (!uploadId) {
      throw new TypeError(C.ERROR_UPLOAD_ID_REQUIRED);
    }

    const query = { uploadId };
    const headers = { [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE, ...(ssecHeaders ? { ...ssecHeaders } : {}) };

    const res = await this._signedRequest('DELETE', key, {
      query,
      headers,
      withQuery: true,
    });
    const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
    if (
      parsed &&
      'error' in parsed &&
      typeof parsed.error === 'object' &&
      parsed.error !== null &&
      'message' in parsed.error
    ) {
      this._log('error', `${C.ERROR_PREFIX}Failed to abort multipart upload: ${String(parsed.error.message)}`);
      throw new Error(`${C.ERROR_PREFIX}Failed to abort multipart upload: ${String(parsed.error.message)}`);
    }
    return { status: 'Aborted', key, uploadId, response: parsed };
  }

  private _buildCompleteMultipartUploadXml(parts: Array<IT.UploadPart>): string {
    return `
      <CompleteMultipartUpload>
        ${parts
          .map(
            part => `
          <Part>
            <PartNumber>${part.partNumber}</PartNumber>
            <ETag>${part.etag}</ETag>
          </Part>
        `,
          )
          .join('')}
      </CompleteMultipartUpload>
    `;
  }

  /**
   * Deletes an object from the bucket.
   * This method sends a request to delete the specified object from the bucket.
   * @param {string} key - The key of the object to delete.
   * @returns A promise that resolves to true if the object was deleted successfully, false otherwise.
   */
  public async deleteObject(key: string): Promise<boolean> {
    const res = await this._signedRequest('DELETE', key, { tolerated: [200, 204] });
    return res.status === 200 || res.status === 204;
  }

  private async _deleteObjectsProcess(keys: string[]): Promise<boolean[]> {
    const xmlBody = `<Delete>${keys.map(key => `<Object><Key>${U.escapeXml(key)}</Key></Object>`).join('')}</Delete>`;
    const query = { delete: '' };
    const md5Base64 = U.md5base64(xmlBody);
    const headers = {
      [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
      [C.HEADER_CONTENT_LENGTH]: Buffer.byteLength(xmlBody).toString(),
      'Content-MD5': md5Base64,
    };

    const res = await this._signedRequest('POST', '', {
      query,
      body: xmlBody,
      headers,
      withQuery: true,
    });
    const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
    if (!parsed || typeof parsed !== 'object') {
      throw new Error(`${C.ERROR_PREFIX}Failed to delete objects: ${JSON.stringify(parsed)}`);
    }
    const out = (parsed.DeleteResult || parsed.deleteResult || parsed) as Record<string, unknown>;
    const resultMap = new Map<string, boolean>();
    keys.forEach(key => resultMap.set(key, false));
    const deleted = out.deleted || out.Deleted;
    if (deleted) {
      const deletedArray = Array.isArray(deleted) ? deleted : [deleted];
      deletedArray.forEach((item: unknown) => {
        if (item && typeof item === 'object') {
          const obj = item as Record<string, unknown>;
          // Check both key and Key
          const key = obj.key || obj.Key;
          if (key && typeof key === 'string') {
            resultMap.set(key, true);
          }
        }
      });
    }

    // Handle errors (check both cases)
    const errors = out.error || out.Error;
    if (errors) {
      const errorsArray = Array.isArray(errors) ? errors : [errors];
      errorsArray.forEach((item: unknown) => {
        if (item && typeof item === 'object') {
          const obj = item as Record<string, unknown>;
          // Check both cases for all properties
          const key = obj.key || obj.Key;
          const code = obj.code || obj.Code;
          const message = obj.message || obj.Message;

          if (key && typeof key === 'string') {
            resultMap.set(key, false);
            // Optionally log the error for debugging
            this._log('warn', `Failed to delete object: ${key}`, {
              code: code || 'Unknown',
              message: message || 'Unknown error',
            });
          }
        }
      });
    }

    // Return boolean array in the same order as input keys
    return keys.map(key => resultMap.get(key) || false);
  }

  /**
   * Deletes multiple objects from the bucket.
   * @param {string[]} keys - An array of object keys to delete.
   * @returns A promise that resolves to an array of booleans indicating success for each key in order.
   */
  public async deleteObjects(keys: string[]): Promise<boolean[]> {
    if (!Array.isArray(keys) || keys.length === 0) {
      return [];
    }
    const maxBatchSize = 1000; // S3 limit for delete batch size
    if (keys.length > maxBatchSize) {
      const allPromises = [];
      for (let i = 0; i < keys.length; i += maxBatchSize) {
        const batch = keys.slice(i, i + maxBatchSize);
        allPromises.push(this._deleteObjectsProcess(batch));
      }
      const results = await Promise.all(allPromises);
      // Flatten the results array
      return results.flat();
    } else {
      return await this._deleteObjectsProcess(keys);
    }
  }

  private async _sendRequest(
    url: string,
    method: IT.HttpMethod,
    headers: Record<string, string>,
    body?: string | Buffer,
    toleratedStatusCodes: number[] = [],
  ): Promise<Response> {
    this._log('info', `Sending ${method} request to ${url}`, `headers: ${JSON.stringify(headers)}`);
    try {
      const res = await fetch(url, {
        method,
        headers,
        body: ['GET', 'HEAD'].includes(method) ? undefined : (body as string),
        signal: this.requestAbortTimeout !== undefined ? AbortSignal.timeout(this.requestAbortTimeout) : undefined,
      });
      this._log('info', `Response status: ${res.status}, tolerated: ${toleratedStatusCodes.join(',')}`);
      if (!res.ok && !toleratedStatusCodes.includes(res.status)) {
        await this._handleErrorResponse(res);
      }
      return res;
    } catch (err: unknown) {
      const code = U.extractErrCode(err);
      if (code && ['ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'ECONNREFUSED'].includes(code)) {
        throw new U.S3NetworkError(`S3 network error: ${code}`, code, err);
      }
      throw err;
    }
  }

  private async _handleErrorResponse(res: Response): Promise<void> {
    const errorBody = await res.text();
    const svcCode = res.headers.get('x-amz-error-code') ?? 'Unknown';
    const errorMessage = res.headers.get('x-amz-error-message') || res.statusText;
    this._log(
      'error',
      `${C.ERROR_PREFIX}Request failed with status ${res.status}: ${svcCode} - ${errorMessage},err body: ${errorBody}`,
    );
    throw new U.S3ServiceError(`S3 returned ${res.status} – ${svcCode}`, res.status, svcCode, errorBody);
  }

  private _buildCanonicalQueryString(queryParams: Record<string, unknown>): string {
    if (!queryParams || Object.keys(queryParams).length === 0) {
      return '';
    }
    return Object.keys(queryParams)
      .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key] as string)}`)
      .sort()
      .join('&');
  }
  private _getSignatureKey(dateStamp: string): Buffer {
    const kDate = U.hmac(`AWS4${this.secretAccessKey}`, dateStamp) as Buffer;
    const kRegion = U.hmac(kDate, this.region) as Buffer;
    const kService = U.hmac(kRegion, C.S3_SERVICE) as Buffer;
    return U.hmac(kService, C.AWS_REQUEST_TYPE) as Buffer;
  }
}

/**
 * @deprecated Use `S3mini` instead.
 */
const s3mini = S3mini;

export { S3mini, s3mini };
export default S3mini;
