import { randomInt } from 'node:crypto';
import { Readable } from 'node:stream';

import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { FileType } from '../interfaces/file.interface';
import { S3UploadResult } from '../interfaces/s3.interface';

@Injectable()
export class S3Service {
  private readonly logger = new Logger(S3Service.name);
  private readonly s3Client: S3Client;
  private readonly bucketName: string;
  private readonly region: string;
  private readonly endpointUrl: string;

  constructor(private readonly configService: ConfigService) {
    this.bucketName = this.configService.getOrThrow<string>('BUCKET_NAME');
    this.endpointUrl = this.configService.getOrThrow<string>('PARSPACK_S3_ENDPOINT');
    this.region = this.configService.getOrThrow<string>('BUCKET_REGION');

    this.s3Client = new S3Client({
      region: this.region,
      endpoint: this.endpointUrl,
      forcePathStyle: true, // Required for ParsPack S3-compatible API
      credentials: {
        accessKeyId: this.configService.getOrThrow<string>('BUCKET_ACCESS_KEY'),
        secretAccessKey: this.configService.getOrThrow<string>('BUCKET_SECRET_KEY'),
      },
    });
  }

  /**
   * Upload file to S3
   */
  async uploadFile(
    buffer: Buffer,
    key: string,
    contentType: string,
    metadata?: Record<string, string>,
  ): Promise<S3UploadResult> {
    try {
      const sanitizedMetadata = metadata
        ? Object.entries(metadata).reduce(
            (acc, [metaKey, value]) => {
              // S3 user metadata must be US-ASCII; base64 keeps signing stable for Unicode names
              acc[metaKey] = Buffer.from(value, 'utf8').toString('base64');
              return acc;
            },
            {} as Record<string, string>,
          )
        : undefined;
      // NOTE: 'ACL: public-read' might not be supported or necessary for ParsPack S3.
      // If uploads fail, try removing this line.
      const command = new PutObjectCommand({
        Bucket: this.bucketName,
        Key: key,
        Body: buffer,
        // ACL: 'public-read',
        ContentType: contentType,
        Metadata: sanitizedMetadata,
      });

      await this.s3Client.send(command);

      const encodedKey = key.split('/').map(segment => encodeURIComponent(segment)).join('/');
      const url = `${this.endpointUrl}/${this.bucketName}/${encodedKey}`;

      this.logger.log(`File uploaded to S3: ${key}`);

      return {
        key,
        url,
        bucket: this.bucketName,
      };
    } catch (error: unknown) {
      this.logger.error(`Failed to upload file to S3: ${(error as Error).message}`, (error as Error).stack);
      throw error;
    }
  }


  /**
   * Get file stream from S3
   */

  async getFileStream(key: string): Promise<Readable> {
    try {
      const command = new GetObjectCommand({
        Bucket: this.bucketName,
        Key: key,
      });

      const response = await this.s3Client.send(command);

      // ✅ VALIDATION
      if (response.Body instanceof Readable) {
        return response.Body;
      }

      // If it's not a Readable stream, throw a specific error
      throw new Error(`File body is not a readable stream for key: ${key}`);
    } catch (error: unknown) {
      this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack);
      throw error;
    }
  }
  /**
   * Generate presigned URL for file download
   */
  async getPresignedUrl(key: string, expiresIn: number = 3600): Promise<string> {
    try {
      const command = new GetObjectCommand({
        Bucket: this.bucketName,
        Key: key,
      });

      return await getSignedUrl(this.s3Client, command, { expiresIn });
    } catch (error: unknown) {
      this.logger.error(`Failed to generate presigned URL: ${(error as Error).message}`, (error as Error).stack);
      throw error;
    }
  }

  /**
   * Delete file from S3
   */
  async deleteFile(key: string): Promise<void> {
    try {
      const command = new DeleteObjectCommand({
        Bucket: this.bucketName,
        Key: key,
      });

      await this.s3Client.send(command);
      this.logger.log(`File deleted from S3: ${key}`);
    } catch (error: unknown) {
      this.logger.error(`Failed to delete file from S3: ${(error as Error).message}`, (error as Error).stack);
      throw error;
    }
  }

  /**
   * Generate S3 key for file
   */
  generateFileKey(originalName: string, fileType: FileType, extensionOverride?: string): string {
    const timestamp = Date.now();
    const randomSuffix = randomInt(1000000, 9999999);
    const extension = extensionOverride ?? this.extractSafeExtension(originalName);

    const basePathMap: Record<FileType, string> = {
      image: 'images',
      video: 'videos',
      audio: 'audio',
      document: 'documents',
      text: 'texts',
      unknown: 'unknowns',
    };

    const basePath = basePathMap[fileType];
    return `${basePath}/${timestamp}-${randomSuffix}.${extension}`;
  }

  /**
   * Extract a safe ASCII extension so Persian/spaced names never end up in the S3 object key.
   */
  private extractSafeExtension(originalName: string): string {
    const basename = originalName.replace(/\\/g, '/').split('/').pop() ?? 'file';
    const lastDot = basename.lastIndexOf('.');

    if (lastDot > 0 && lastDot < basename.length - 1) {
      const ext = basename.slice(lastDot + 1).toLowerCase();
      if (/^[a-z0-9]{1,16}$/.test(ext)) {
        return ext;
      }
    }

    return 'bin';
  }
}
