/**
 * S3StorageProvider
 *
 * AWS S3 storage implementation following the Strategy Pattern.
 * Implements BaseStorageProvider interface for S3-specific operations.
 *
 * Features:
 * - Request timeout and connection pool management
 * - Retry strategy with exponential backoff
 * - AbortController support for request cancellation
 * - Comprehensive error handling
 */

const https = require('https');
const {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
  DeleteObjectsCommand,
  ListObjectsV2Command,
  HeadObjectCommand,
} = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const { NodeHttpHandler } = require('@smithy/node-http-handler');
const BaseStorageProvider = require('./BaseStorageProvider');

/**
 * Map S3 error names to HTTP status codes
 */
const S3_ERROR_STATUS_MAP = {
  NoSuchKey: 404,
  NotFound: 404,
  NoSuchBucket: 404,
  AccessDenied: 403,
  InvalidAccessKeyId: 403,
  SignatureDoesNotMatch: 403,
  InvalidObjectState: 403,
  ExpiredToken: 401,
  TimeoutError: 504,
  RequestTimeout: 504,
  NetworkingError: 503,
  ServiceUnavailable: 503,
  SlowDown: 503,
  InternalError: 500,
  ThrottlingException: 429,
  TooManyRequestsException: 429,
};

/**
 * Errors that should be retried
 */
const RETRYABLE_ERRORS = new Set([
  'TimeoutError',
  'RequestTimeout',
  'NetworkingError',
  'ServiceUnavailable',
  'SlowDown',
  'InternalError',
  'ThrottlingException',
  'TooManyRequestsException',
  'ECONNRESET',
  'ECONNREFUSED',
  'ETIMEDOUT',
  'EPIPE',
]);

class S3StorageProvider extends BaseStorageProvider {
  /**
   * @param {Object} options
   * @param {string} options.bucket - S3 bucket name
   * @param {string} options.region - AWS region
   * @param {string} [options.accessKeyId] - AWS access key ID
   * @param {string} [options.secretAccessKey] - AWS secret access key
   * @param {string} [options.prefix] - Key prefix for all operations
   * @param {number} [options.connectionTimeout=5000] - Connection timeout in ms
   * @param {number} [options.requestTimeout=30000] - Request timeout in ms
   * @param {number} [options.maxAttempts=3] - Maximum retry attempts
   * @param {number} [options.maxSockets=50] - Maximum concurrent connections
   * @param {boolean} [options.keepAlive=true] - Enable connection keep-alive
   */
  constructor(options = {}) {
    super();
    this.bucket = options.bucket;
    this.prefix = options.prefix || '';

    // Timeout and connection pool settings
    const connectionTimeout = options.connectionTimeout || 5000;
    const requestTimeout = options.requestTimeout || 30000;
    const maxSockets = options.maxSockets || 50;
    const keepAlive = options.keepAlive !== false;

    // Create HTTPS agent with connection pooling
    this.httpsAgent = new https.Agent({
      maxSockets,
      keepAlive,
      keepAliveMsecs: 1000,
    });

    // Create NodeHttpHandler with timeout and connection pool
    const requestHandler = new NodeHttpHandler({
      connectionTimeout,
      requestTimeout,
      httpsAgent: this.httpsAgent,
    });

    // Retry configuration
    const maxAttempts = options.maxAttempts || 3;

    this.client = new S3Client({
      region: options.region || 'us-east-1',
      credentials:
        options.accessKeyId && options.secretAccessKey
          ? {
              accessKeyId: options.accessKeyId,
              secretAccessKey: options.secretAccessKey,
            }
          : undefined,
      requestHandler,
      maxAttempts,
      retryMode: 'adaptive', // Use adaptive retry with exponential backoff
    });

    // Store config for health checks and logging
    this.config = {
      region: options.region || 'us-east-1',
      connectionTimeout,
      requestTimeout,
      maxAttempts,
      maxSockets,
      keepAlive,
    };
  }

  static get providerName() {
    return 's3';
  }

  /**
   * Get HTTP status code for an S3 error
   * @param {Error} error - The error object
   * @returns {number} HTTP status code
   */
  static getErrorStatusCode(error) {
    if (!error) return 500;

    // Check by error name
    if (error.name && S3_ERROR_STATUS_MAP[error.name]) {
      return S3_ERROR_STATUS_MAP[error.name];
    }

    // Check by error code (for network errors)
    if (error.code && RETRYABLE_ERRORS.has(error.code)) {
      return 503;
    }

    // Check by $metadata (AWS SDK v3 format)
    if (error.$metadata?.httpStatusCode) {
      return error.$metadata.httpStatusCode;
    }

    return 500;
  }

  /**
   * Check if an error is retryable
   * @param {Error} error - The error object
   * @returns {boolean}
   */
  static isRetryableError(error) {
    if (!error) return false;
    return (
      RETRYABLE_ERRORS.has(error.name) ||
      RETRYABLE_ERRORS.has(error.code) ||
      (error.$metadata?.httpStatusCode >= 500 &&
        error.$metadata?.httpStatusCode < 600)
    );
  }

  /**
   * Build full key with prefix
   */
  buildKey(key) {
    const cleanPrefix = (this.prefix || '').replace(/^\/+|\/+$/g, '');
    const cleanKey = (key || '').replace(/^\/+/, '');
    return cleanPrefix ? `${cleanPrefix}/${cleanKey}` : cleanKey;
  }

  /**
   * Upload a file to S3
   * @param {string} key - Storage key/path
   * @param {Buffer|ReadableStream} data - File data
   * @param {Object} options - Upload options
   * @param {AbortSignal} [options.signal] - AbortController signal for cancellation
   * @returns {Promise<{ key: string, url?: string }>}
   */
  async upload(key, data, options = {}) {
    const fullKey = this.buildKey(key);
    const { signal, ...uploadOptions } = options;

    const params = {
      Bucket: this.bucket,
      Key: fullKey,
      Body: data,
    };

    if (uploadOptions.contentType) {
      params.ContentType = uploadOptions.contentType;
    }

    if (uploadOptions.metadata) {
      params.Metadata = uploadOptions.metadata;
    }

    const sendOptions = signal ? { abortSignal: signal } : {};
    await this.client.send(new PutObjectCommand(params), sendOptions);

    return {
      key: fullKey,
      url: `https://${this.bucket}.s3.amazonaws.com/${fullKey}`,
    };
  }

  /**
   * Download a file from S3
   * @param {string} key - Storage key/path
   * @param {Object} [options] - Download options
   * @param {AbortSignal} [options.signal] - AbortController signal for cancellation
   * @returns {Promise<{ body: ReadableStream, contentType?: string, contentLength?: number }>}
   */
  async download(key, options = {}) {
    const fullKey = this.buildKey(key);
    const { signal } = options;

    const sendOptions = signal ? { abortSignal: signal } : {};
    const output = await this.client.send(
      new GetObjectCommand({
        Bucket: this.bucket,
        Key: fullKey,
      }),
      sendOptions,
    );

    return {
      body: output.Body,
      contentType: output.ContentType,
      contentLength: output.ContentLength,
    };
  }

  /**
   * Delete a file from S3
   * @param {string} key - Storage key/path
   * @param {Object} [options] - Delete options
   * @param {AbortSignal} [options.signal] - AbortController signal for cancellation
   * @returns {Promise<void>}
   */
  async delete(key, options = {}) {
    const fullKey = this.buildKey(key);
    const { signal } = options;

    const sendOptions = signal ? { abortSignal: signal } : {};
    await this.client.send(
      new DeleteObjectCommand({
        Bucket: this.bucket,
        Key: fullKey,
      }),
      sendOptions,
    );
  }

  /**
   * Delete multiple files from S3
   * @param {string[]} keys - Array of keys to delete
   * @param {Object} [options] - Delete options
   * @param {AbortSignal} [options.signal] - AbortController signal for cancellation
   * @returns {Promise<{ deleted: string[], errors: Array<{ key: string, error: string }> }>}
   */
  async deleteMany(keys, options = {}) {
    if (!keys || keys.length === 0) {
      return { deleted: [], errors: [] };
    }

    const { signal } = options;
    const sendOptions = signal ? { abortSignal: signal } : {};
    const objects = keys.map((key) => ({ Key: this.buildKey(key) }));
    const deleted = [];
    const errors = [];

    // S3 DeleteObjects supports max 1000 objects per request
    const chunks = [];
    for (let i = 0; i < objects.length; i += 1000) {
      chunks.push(objects.slice(i, i + 1000));
    }

    for (const chunk of chunks) {
      const result = await this.client.send(
        new DeleteObjectsCommand({
          Bucket: this.bucket,
          Delete: { Objects: chunk },
        }),
        sendOptions,
      );

      if (result.Deleted) {
        deleted.push(...result.Deleted.map((d) => d.Key));
      }
      if (result.Errors) {
        errors.push(
          ...result.Errors.map((e) => ({
            key: e.Key,
            error: e.Message || e.Code,
          })),
        );
      }
    }

    return { deleted, errors };
  }

  /**
   * Check if a file exists in S3
   * @param {string} key - Storage key/path
   * @param {Object} [options] - Options
   * @param {AbortSignal} [options.signal] - AbortController signal for cancellation
   * @returns {Promise<boolean>}
   */
  async exists(key, options = {}) {
    const fullKey = this.buildKey(key);
    const { signal } = options;

    try {
      const sendOptions = signal ? { abortSignal: signal } : {};
      await this.client.send(
        new HeadObjectCommand({
          Bucket: this.bucket,
          Key: fullKey,
        }),
        sendOptions,
      );
      return true;
    } catch (error) {
      if (error.name === 'NotFound' || error.name === 'NoSuchKey') {
        return false;
      }
      throw error;
    }
  }

  /**
   * List files with a given prefix
   * @param {string} prefix - Key prefix
   * @param {Object} [options] - Options
   * @param {AbortSignal} [options.signal] - AbortController signal for cancellation
   * @returns {Promise<string[]>} Array of keys
   */
  async list(prefix, options = {}) {
    const fullPrefix = this.buildKey(prefix);
    const { signal } = options;
    const sendOptions = signal ? { abortSignal: signal } : {};
    const keys = [];
    let continuationToken = null;

    do {
      const params = {
        Bucket: this.bucket,
        Prefix: fullPrefix,
      };

      if (continuationToken) {
        params.ContinuationToken = continuationToken;
      }

      const result = await this.client.send(
        new ListObjectsV2Command(params),
        sendOptions,
      );

      if (result.Contents) {
        keys.push(...result.Contents.map((obj) => obj.Key));
      }

      continuationToken = result.IsTruncated
        ? result.NextContinuationToken
        : null;
    } while (continuationToken);

    return keys;
  }

  /**
   * Get a signed URL for direct access
   * @param {string} key - Storage key/path
   * @param {number} expiresIn - Expiration time in seconds
   * @returns {Promise<string>} Signed URL
   */
  async getSignedUrl(key, expiresIn = 3600) {
    const fullKey = this.buildKey(key);

    const command = new GetObjectCommand({
      Bucket: this.bucket,
      Key: fullKey,
    });

    return getSignedUrl(this.client, command, { expiresIn });
  }

  /**
   * Get the underlying S3 client for advanced operations
   * @returns {S3Client}
   */
  getClient() {
    return this.client;
  }

  /**
   * Get bucket name
   * @returns {string}
   */
  getBucket() {
    return this.bucket;
  }

  /**
   * Get prefix
   * @returns {string}
   */
  getPrefix() {
    return this.prefix;
  }

  /**
   * Get provider configuration (for logging/debugging)
   * @returns {Object}
   */
  getConfig() {
    return { ...this.config };
  }

  /**
   * Cleanup resources (close connections)
   */
  destroy() {
    if (this.httpsAgent) {
      this.httpsAgent.destroy();
    }
  }
}

module.exports = S3StorageProvider;
