import {
  Injectable,
  Logger,
  InternalServerErrorException,
  NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as Minio from 'minio';

@Injectable()
export class MinioService {
  private readonly logger = new Logger(MinioService.name);
  private readonly minioClient: Minio.Client;
  private readonly defaultBucket: string;
  private readonly region: string;

  constructor(private readonly configService: ConfigService) {
    const endPoint = this.configService.get<string>('MINIO_ENDPOINT', 'localhost');
    const port = this.configService.get<number>('MINIO_PORT', 9000);
    const useSSL = this.configService.get<boolean>('MINIO_USE_SSL', false);
    const accessKey = this.configService.get<string>('MINIO_ACCESS_KEY', 'minioadmin');
    const secretKey = this.configService.get<string>('MINIO_SECRET_KEY', 'minioadmin');

    this.defaultBucket = this.configService.get<string>('MINIO_BUCKET', 'goa-gel-documents');
    this.region = this.configService.get<string>('MINIO_REGION', 'us-east-1');

    this.minioClient = new Minio.Client({
      endPoint,
      port,
      useSSL,
      accessKey,
      secretKey,
      region: this.region,
    });

    this.logger.log(`MinIO client initialized: ${endPoint}:${port}, bucket: ${this.defaultBucket}`);
  }

  async uploadFile(
    bucket: string,
    path: string,
    buffer: Buffer,
    metadata: Record<string, string> = {},
  ): Promise<string> {
    this.logger.debug(`Uploading file to MinIO: ${bucket}/${path}`);

    try {
      await this.ensureBucketExists(bucket);

      const fullPath = path;
      await this.minioClient.putObject(bucket, fullPath, buffer, buffer.length, {
        ...metadata,
        'Content-Type': metadata['Content-Type'] || 'application/octet-stream',
      });

      this.logger.log(`File uploaded successfully: ${bucket}/${fullPath}`);
      return fullPath;
    } catch (error: any) {
      this.logger.error(`Failed to upload file: ${error.message}`);
      throw new InternalServerErrorException('Failed to upload file to storage');
    }
  }

  async getSignedUrl(bucket: string, path: string, expiresIn: number = 3600): Promise<string> {
    this.logger.debug(`Generating signed URL for: ${bucket}/${path}`);

    try {
      // Verify file exists before generating URL
      try {
        await this.minioClient.statObject(bucket, path);
      } catch (statError: any) {
        if (
          statError.code === 'NotFound' ||
          statError.message?.includes('Not Found') ||
          statError.code === 'NoSuchKey'
        ) {
          this.logger.error(`File not found for signed URL: ${bucket}/${path}`);
          throw new NotFoundException(`File not found in storage: ${path}`);
        }
        throw statError;
      }

      const url = await this.minioClient.presignedGetObject(bucket, path, expiresIn);
      this.logger.debug(`Signed URL generated successfully`);
      return url;
    } catch (error: any) {
      if (error instanceof NotFoundException) {
        throw error;
      }
      this.logger.error(`Failed to generate signed URL: ${error.message}`);
      throw new InternalServerErrorException('Failed to generate download URL');
    }
  }

  async deleteFile(bucket: string, path: string): Promise<void> {
    this.logger.debug(`Deleting file from MinIO: ${bucket}/${path}`);

    try {
      await this.minioClient.removeObject(bucket, path);
      this.logger.log(`File deleted successfully: ${bucket}/${path}`);
    } catch (error: any) {
      this.logger.error(`Failed to delete file: ${error.message}`);
      throw new InternalServerErrorException('Failed to delete file from storage');
    }
  }

  async ensureBucketExists(bucket: string): Promise<void> {
    try {
      const exists = await this.minioClient.bucketExists(bucket);

      if (!exists) {
        this.logger.debug(`Creating MinIO bucket: ${bucket}`);
        await this.minioClient.makeBucket(bucket, this.region);
        this.logger.log(`Bucket created: ${bucket}`);

        await this.setDefaultBucketPolicy(bucket);
      }
    } catch (error: any) {
      this.logger.error(`Failed to ensure bucket exists: ${error.message}`);
      throw new InternalServerErrorException('Failed to initialize storage bucket');
    }
  }

  async getFileMetadata(bucket: string, path: string): Promise<Minio.BucketItemStat> {
    this.logger.debug(`Fetching file metadata: ${bucket}/${path}`);

    try {
      const stat = await this.minioClient.statObject(bucket, path);
      return stat;
    } catch (error: any) {
      if (
        error.code === 'NotFound' ||
        error.message?.includes('Not Found') ||
        error.code === 'NoSuchKey'
      ) {
        this.logger.error(`File not found: ${bucket}/${path}`);
        throw new NotFoundException(`File not found in storage: ${path}`);
      }
      this.logger.error(`Failed to get file metadata: ${error.message}`);
      throw new InternalServerErrorException('Failed to retrieve file metadata');
    }
  }

  async getFile(bucket: string, path: string): Promise<Buffer> {
    this.logger.debug(`Fetching file from MinIO: ${bucket}/${path}`);

    try {
      // First check if the bucket exists
      const bucketExists = await this.minioClient.bucketExists(bucket);
      if (!bucketExists) {
        this.logger.error(`Bucket does not exist: ${bucket}`);
        throw new NotFoundException(`Storage bucket not found: ${bucket}`);
      }

      // Check if the file exists before trying to retrieve it
      try {
        await this.minioClient.statObject(bucket, path);
      } catch (statError: any) {
        if (
          statError.code === 'NotFound' ||
          statError.message?.includes('Not Found') ||
          statError.code === 'NoSuchKey'
        ) {
          this.logger.error(`File not found in storage: ${bucket}/${path}`);
          throw new NotFoundException(`File not found in storage: ${path}`);
        }
        throw statError;
      }

      const dataStream = await this.minioClient.getObject(bucket, path);
      const chunks: Buffer[] = [];

      return new Promise((resolve, reject) => {
        dataStream.on('data', chunk => chunks.push(chunk));
        dataStream.on('end', () => resolve(Buffer.concat(chunks)));
        dataStream.on('error', err => {
          this.logger.error(`Stream error while fetching file: ${err.message}`);
          reject(new InternalServerErrorException('Failed to retrieve file from storage'));
        });
      });
    } catch (error: any) {
      // Re-throw NotFoundException as-is
      if (error instanceof NotFoundException) {
        throw error;
      }
      this.logger.error(`Failed to get file: ${error.message}`, error.stack);
      throw new InternalServerErrorException('Failed to retrieve file from storage');
    }
  }

  private async setDefaultBucketPolicy(bucket: string): Promise<void> {
    try {
      const policy = {
        Version: '2012-10-17',
        Statement: [
          {
            Effect: 'Allow',
            Principal: {
              AWS: '*',
            },
            Action: ['s3:GetObject'],
            Resource: [`arn:aws:s3:::${bucket}/*`],
          },
        ],
      };

      await this.minioClient.setBucketPolicy(bucket, JSON.stringify(policy));
      this.logger.debug(`Bucket policy set for: ${bucket}`);
    } catch (error: any) {
      this.logger.warn(`Failed to set bucket policy: ${error.message}`);
    }
  }

  getDefaultBucket(): string {
    return this.defaultBucket;
  }

  /**
   * Check if MinIO service is healthy and can be reached
   */
  async isHealthy(): Promise<boolean> {
    try {
      // Try to list buckets as a health check
      await this.minioClient.listBuckets();
      return true;
    } catch (error: any) {
      this.logger.error(`MinIO health check failed: ${error.message}`);
      return false;
    }
  }

  /**
   * Check if a specific file exists in storage
   */
  async fileExists(bucket: string, path: string): Promise<boolean> {
    try {
      await this.minioClient.statObject(bucket, path);
      return true;
    } catch (error: any) {
      if (
        error.code === 'NotFound' ||
        error.message?.includes('Not Found') ||
        error.code === 'NoSuchKey'
      ) {
        return false;
      }
      // For other errors, log and return false
      this.logger.error(`Error checking file existence: ${error.message}`);
      return false;
    }
  }
}
