/**
 * S3 Storage Adapter
 *
 * Implements StoragePort using AWS S3
 */

import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
  HeadObjectCommand,
  HeadBucketCommand,
  CreateBucketCommand,
  ListObjectsV2Command,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import {
  StoragePort,
  UploadOptions,
  DownloadOptions,
  DeleteOptions,
  StorageObject,
} from '@domain/ports/out/storage.port';

@Injectable()
export class S3StorageAdapter implements StoragePort {
  private readonly logger = new Logger(S3StorageAdapter.name);
  private s3Client: S3Client;

  constructor(private readonly configService: ConfigService) {
    this.initializeS3Client();
  }

  private initializeS3Client(): void {
    const region = this.configService.get<string>('AWS_REGION', 'us-east-1');
    const endpoint = this.configService.get<string>('AWS_S3_ENDPOINT');
    const accessKeyId = this.configService.get<string>('AWS_ACCESS_KEY_ID');
    const secretAccessKey = this.configService.get<string>('AWS_SECRET_ACCESS_KEY');

    // Check if S3/MinIO is configured
    const isConfigured = endpoint || (accessKeyId && secretAccessKey);

    if (!isConfigured) {
      this.logger.warn(
        'S3 Storage adapter is NOT configured (no endpoint or credentials). Storage operations will fail. ' +
          'Set AWS_S3_ENDPOINT for MinIO or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY for AWS S3.'
      );
      // Don't initialize client if not configured
      return;
    }

    this.s3Client = new S3Client({
      region,
      endpoint,
      credentials:
        accessKeyId && secretAccessKey
          ? {
              accessKeyId,
              secretAccessKey,
            }
          : undefined,
      forcePathStyle: !!endpoint, // Required for MinIO
    });

    this.logger.log(
      `S3 Storage adapter initialized with region: ${region}${
        endpoint ? ` (endpoint: ${endpoint})` : ''
      }`
    );
  }

  async ensureBucket(bucket: string): Promise<void> {
    if (!this.s3Client) return;

    try {
      await this.s3Client.send(new HeadBucketCommand({ Bucket: bucket }));
    } catch (err: any) {
      const status = err.$metadata?.httpStatusCode;
      if (status === 404 || err.name === 'NoSuchBucket' || err.name === 'NotFound') {
        this.logger.log(`Bucket "${bucket}" not found — creating it automatically`);
        await this.s3Client.send(new CreateBucketCommand({ Bucket: bucket }));
        this.logger.log(`Bucket "${bucket}" created`);
      } else {
        throw err;
      }
    }
  }

  async upload(options: UploadOptions): Promise<StorageObject> {
    if (!this.s3Client) {
      throw new Error(
        'S3 Storage is not configured. Set AWS_S3_ENDPOINT or AWS credentials in .env'
      );
    }

    await this.ensureBucket(options.bucket);

    try {
      const command = new PutObjectCommand({
        Bucket: options.bucket,
        Key: options.key,
        Body: options.body,
        ContentType: options.contentType,
        Metadata: options.metadata,
        // ACL is deprecated in favor of bucket policies
      });

      await this.s3Client.send(command);

      const url = this.buildUrl(options.bucket, options.key);
      const size =
        typeof options.body === 'string' ? Buffer.byteLength(options.body) : options.body.length;

      this.logger.log(`Uploaded file to S3: ${options.key}`);

      return {
        key: options.key,
        url,
        size,
        contentType: options.contentType,
      };
    } catch (error) {
      this.logger.error(`Failed to upload file to S3: ${options.key}`, error);
      throw error;
    }
  }

  async download(options: DownloadOptions): Promise<Buffer> {
    if (!this.s3Client) {
      throw new Error(
        'S3 Storage is not configured. Set AWS_S3_ENDPOINT or AWS credentials in .env'
      );
    }

    try {
      const command = new GetObjectCommand({
        Bucket: options.bucket,
        Key: options.key,
      });

      const response = await this.s3Client.send(command);
      const stream = response.Body as any;

      const chunks: Uint8Array[] = [];
      for await (const chunk of stream) {
        chunks.push(chunk);
      }

      this.logger.log(`Downloaded file from S3: ${options.key}`);
      return Buffer.concat(chunks);
    } catch (error) {
      this.logger.error(`Failed to download file from S3: ${options.key}`, error);
      throw error;
    }
  }

  async delete(options: DeleteOptions): Promise<void> {
    try {
      const command = new DeleteObjectCommand({
        Bucket: options.bucket,
        Key: options.key,
      });

      await this.s3Client.send(command);
      this.logger.log(`Deleted file from S3: ${options.key}`);
    } catch (error) {
      this.logger.error(`Failed to delete file from S3: ${options.key}`, error);
      throw error;
    }
  }

  async getSignedUrl(options: DownloadOptions, expiresIn: number = 3600): Promise<string> {
    try {
      const command = new GetObjectCommand({
        Bucket: options.bucket,
        Key: options.key,
      });

      const url = await getSignedUrl(this.s3Client, command, { expiresIn });
      this.logger.log(`Generated signed URL for: ${options.key} (expires in ${expiresIn}s)`);
      return url;
    } catch (error) {
      this.logger.error(`Failed to generate signed URL for: ${options.key}`, error);
      throw error;
    }
  }

  async exists(options: DownloadOptions): Promise<boolean> {
    try {
      const command = new HeadObjectCommand({
        Bucket: options.bucket,
        Key: options.key,
      });

      await this.s3Client.send(command);
      return true;
    } catch (error: any) {
      if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
        return false;
      }
      this.logger.error(`Error checking if file exists: ${options.key}`, error);
      throw error;
    }
  }

  async list(bucket: string, prefix?: string): Promise<StorageObject[]> {
    try {
      const command = new ListObjectsV2Command({
        Bucket: bucket,
        Prefix: prefix,
      });

      const response = await this.s3Client.send(command);
      const objects: StorageObject[] = [];

      if (response.Contents) {
        for (const item of response.Contents) {
          if (item.Key) {
            objects.push({
              key: item.Key,
              url: this.buildUrl(bucket, item.Key),
              size: item.Size || 0,
              lastModified: item.LastModified,
            });
          }
        }
      }

      this.logger.log(
        `Listed ${objects.length} objects from S3 bucket: ${bucket}${
          prefix ? ` with prefix: ${prefix}` : ''
        }`
      );
      return objects;
    } catch (error) {
      this.logger.error(`Failed to list objects from S3 bucket: ${bucket}`, error);
      throw error;
    }
  }

  private buildUrl(bucket: string, key: string): string {
    const endpoint = this.configService.get<string>('AWS_S3_ENDPOINT');
    const region = this.configService.get<string>('AWS_REGION', 'us-east-1');

    if (endpoint) {
      // MinIO or custom endpoint
      return `${endpoint}/${bucket}/${key}`;
    }

    // AWS S3
    return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
  }
}
